• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rl-institut / multi-vector-simulator / 8870538658

28 Apr 2024 09:31PM UTC coverage: 75.582% (-1.4%) from 76.96%
8870538658

push

github

web-flow
Merge pull request #971 from rl-institut/fix/black-vulnerability

Fix/black vulnerability

26 of 29 new or added lines in 15 files covered. (89.66%)

826 existing lines in 21 files now uncovered.

5977 of 7908 relevant lines covered (75.58%)

0.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

40.96
/src/multi_vector_simulator/F1_plotting.py
1
r"""
2
Module F1 - Plotting
3
====================
4

5
Module F1 describes all the functions that create plots.
6

7
- creating graphs for energy flows
8
- creating bar chart for capacity
9
- creating pie chart for cost data
10
- creating network graph for the model brackets only working on Ubuntu
11
"""
12

13
import logging
1✔
14
import os
1✔
15
import textwrap
1✔
16
import numpy as np
1✔
17

18
import pandas as pd
1✔
19

20
PLOTLY_INSTALLED = False
1✔
21
try:
1✔
22
    import plotly.graph_objs as go
1✔
23
    import plotly.express as px
1✔
24

25
    PLOTLY_INSTALLED = True
1✔
26
except ModuleNotFoundError:
×
UNCOV
27
    logging.warning(
×
28
        "You have installed the minimal configuration, if you want to output images "
29
        "please run the command <TODO>"
30
    )
31

32
import graphviz
1✔
33
import oemof
1✔
34
from oemof import solph
1✔
35

36
from multi_vector_simulator.utils.constants import (
1✔
37
    PROJECT_DATA,
38
    ECONOMIC_DATA,
39
    LABEL,
40
    OUTPUT_FOLDER,
41
    PATHS_TO_PLOTS,
42
    PLOT_SANKEY,
43
    SOC,
44
)
45

46
from multi_vector_simulator.utils.constants_json_strings import (
1✔
47
    PROJECT_NAME,
48
    SCENARIO_NAME,
49
    CURR,
50
    KPI,
51
    UNIT,
52
    ENERGY_CONSUMPTION,
53
    TIMESERIES,
54
    DISPATCHABILITY,
55
    ENERGY_PRODUCTION,
56
    SIMULATION_SETTINGS,
57
    OPTIMIZED_ADD_CAP,
58
    TOTAL_FLOW,
59
    ANNUAL_TOTAL_FLOW,
60
    PEAK_FLOW,
61
    AVERAGE_FLOW,
62
    KPI_SCALAR_MATRIX,
63
    OPTIMIZED_FLOWS,
64
    DEMANDS,
65
    RESOURCES,
66
    TIME_INDEX,
67
)
68

69
from multi_vector_simulator.E1_process_results import (
1✔
70
    convert_demand_to_dataframe,
71
    convert_costs_to_dataframe,
72
)
73

74

75
def convert_plot_data_to_dataframe(plot_data_dict, data_type):
1✔
76
    """
77

78
    Parameters
79
    ----------
80
    plot_data_dict: dict
81
        timeseries for either demand or supply
82

83
    data_type: str
84
        one of DEMANDS or RESOURCES
85

86
    Returns
87
    -------
88
    df: pandas:`pandas.DataFrame<frame>`,
89
        timeseries for plotting
90
    """
91
    # Later, this dataframe can be passed to a function directly make the graphs with Plotly
92
    df = pd.DataFrame.from_dict(plot_data_dict[data_type], orient="columns")
×
93

94
    # Change the index of the dataframe
95
    df.reset_index(level=0, inplace=True)
×
96
    # Rename the timestamp column from 'index' to 'timestamp'
97
    df = df.rename(columns={"index": "timestamp"})
×
98
    return df
×
99

100

101
def extract_plot_data_and_title(dict_values, df_dem=None):
1✔
102
    """Dataframe used for the plots of demands and resources timeseries in the report
103

104
    Parameters
105
    ----------
106
    dict_values: dict
107
        output values of MVS
108

109
    df_dem: :class:`pandas.DataFrame<frame>`
110
        summarized demand information for each demand
111

112
    Returns
113
    -------
114
    :class:`pandas.DataFrame<frame>`
115

116
    """
117
    if df_dem is None:
×
118
        df_dem = convert_demand_to_dataframe(dict_values)
×
119

120
    # Collect the keys of various resources (PV, Wind, etc.)
121
    resources = dict_values[ENERGY_PRODUCTION].copy()
×
122
    res_keys = [k for k in resources.keys() if resources[k][DISPATCHABILITY] is False]
×
123

124
    # Gather all the keys of the various plots for later use in the graphOptions.csv
125
    dict_for_plots = {DEMANDS: {}, RESOURCES: {}}
×
126
    dict_plot_labels = {}
×
127

128
    # Add all the demands to the dict_for_plots dictionary, including the timeseries values
129
    for demand in df_dem.Demands:
×
130
        dict_for_plots[DEMANDS].update(
×
131
            {demand: dict_values[ENERGY_CONSUMPTION][demand][TIMESERIES]}
132
        )
133
        dict_plot_labels.update(
×
134
            {demand: dict_values[ENERGY_CONSUMPTION][demand][LABEL]}
135
        )
136

137
    # Add all the resources to the dict_for_plots dictionary, including the timeseries values
138
    for resource in res_keys:
×
139
        dict_for_plots[RESOURCES].update(
×
140
            {resource: dict_values[ENERGY_PRODUCTION][resource][TIMESERIES]}
141
        )
142
        dict_plot_labels.update(
×
143
            {resource: dict_values[ENERGY_PRODUCTION][resource][LABEL]}
144
        )
145

146
    return dict_for_plots, dict_plot_labels
×
147

148

149
def fixed_width_text(text, char_num=10):
1✔
150
    """Add linebreaks every char_num characters in a given text.
151

152
    Parameters
153
    ----------
154
    text: obj:'str'
155
        text to apply the linebreaks
156
    char_num: obj:'int'
157
        max number of characters in a line before a line break
158
        Default: 10
159
    Returns
160
    -------
161
    obj:'str'
162
        the text with line breaks after every char_num characters
163

164
    """
165
    # total number of characters in the text
166
    text_length = len(text)
1✔
167
    # integer number of lines of `char_num` character
168
    n_lines = int(text_length / char_num)
1✔
169
    # number of character in the last line
170
    last_line_length = text_length % char_num
1✔
171

172
    # split the text in lines of `char_num` character
173
    split_text = []
1✔
174
    for i in range(n_lines):
1✔
175
        split_text.append(text[(i * char_num) : ((i + 1) * char_num)])
1✔
176

177
    # I if the last line is not empty
178
    if n_lines > 0:
1✔
179
        if last_line_length > 0:
1✔
180
            split_text.append(text[((i + 1) * char_num) :])
1✔
181
        answer = "\n".join(split_text)
1✔
182
    else:
183
        answer = text
1✔
184
    return answer
1✔
185

186

187
class ESGraphRenderer:
1✔
188
    def __init__(
1✔
189
        self,
190
        energy_system=None,
191
        filepath="network",
192
        img_format=None,
193
        legend=True,
194
        txt_width=10,
195
        txt_fontsize=10,
196
        **kwargs,
197
    ):
198
        """Draw the energy system with Graphviz.
199

200
        Parameters
201
        ----------
202
        energy_system: `oemof.solph.network.EnergySystem`
203
            The oemof energy stystem
204

205
        filepath: str
206
            path, where the rendered result shall be saved, if an extension is provided, the format
207
            will be automatically adapted except if the `img_format` argument is provided
208
            Default: "network"
209

210
        img_format: str
211
            extension of the available image formats of graphviz (e.g "png", "svg", "pdf", ... )
212
            Default: "pdf"
213

214
        legend: bool
215
            specify, whether a legend will be added to the graph or not
216
            Default: False
217

218
        txt_width: int
219
            max number of characters in a line before a line break
220
            Default: 10
221

222
         txt_fontsize: int
223
            fontsize of the image's text (components labels)
224
            Default: 10
225

226
        Returns
227
        -------
228
        None:
229
        render the generated dot graph in the filepath
230

231
        Notes
232
        -----
233
        When new oemof-solph asset types are added to the available types in the MVS, this function needs to be updated, so that it can render the asset in the graph.
234
        """
235
        file_name, file_ext = os.path.splitext(filepath)
1✔
236

237
        self.energy_system = energy_system
1✔
238

239
        if img_format is None:
1✔
240
            if file_ext != "":
1✔
241
                img_format = file_ext.replace(".", "")
1✔
242
            else:
243
                img_format = "pdf"
×
244

245
        self.dot = graphviz.Digraph(filename=file_name, format=img_format, **kwargs)
1✔
246
        self.txt_width = txt_width
1✔
247
        self.txt_fontsize = str(txt_fontsize)
1✔
248
        self.busses = []
1✔
249

250
        if legend is True:
1✔
251
            with self.dot.subgraph(name="cluster_1") as c:
1✔
252
                # color of the legend box
253
                c.attr(color="black")
1✔
254
                # title of the legend box
255
                c.attr(label="Legends")
1✔
256
                self.add_bus(subgraph=c)
1✔
257
                self.add_sink(subgraph=c)
1✔
258
                self.add_source(subgraph=c)
1✔
259
                self.add_transformer(subgraph=c)
1✔
260
                self.add_storage(subgraph=c)
1✔
261

262
        # draw a node for each of the network's component. The shape depends on the component's type
263
        for nd in self.energy_system.nodes:
1✔
264
            if isinstance(nd, oemof.network.Bus):
1✔
265
                self.add_bus(nd.label)
1✔
266
                # keep the bus reference for drawing edges later
267
                self.busses.append(nd)
1✔
268
            elif isinstance(nd, oemof.network.Sink):
1✔
269
                self.add_sink(nd.label)
1✔
270
            elif isinstance(nd, oemof.network.Source):
1✔
271
                self.add_source(nd.label)
1✔
272
            elif isinstance(nd, oemof.network.Transformer):
1✔
273
                self.add_transformer(nd.label)
1✔
274
            elif isinstance(nd, solph.components.GenericStorage):
1✔
275
                self.add_storage(nd.label)
1✔
276
            else:
277
                logging.warning(
×
278
                    "The component {} of type {} is not implemented in the rendering "
279
                    "function of the energy model network graph drawer. It will be "
280
                    "rendered as an ellipse".format(nd.label, type(nd))
281
                )
282
                self.add_component(nd.label)
×
283

284
        # draw the edges between the nodes based on each bus inputs/outputs
285
        for bus in self.busses:
1✔
286
            for component in bus.inputs:
1✔
287
                # draw an arrow from the component to the bus
288
                self.connect(component, bus)
1✔
289
            for component in bus.outputs:
1✔
290
                # draw an arrow from the bus to the component
291
                self.connect(bus, component)
1✔
292

293
    def add_bus(self, label="Bus", subgraph=None):
1✔
294
        if subgraph is None:
1✔
295
            dot = self.dot
1✔
296
        else:
297
            dot = subgraph
1✔
298
        dot.node(
1✔
299
            label,
300
            shape="rectangle",
301
            fontsize="10",
302
            fixedsize="shape",
303
            width="4.1",
304
            height="0.3",
305
            style="filled",
306
            color="lightgrey",
307
        )
308

309
    def add_sink(self, label="Sink", subgraph=None):
1✔
310
        if subgraph is None:
1✔
311
            dot = self.dot
1✔
312
        else:
313
            dot = subgraph
1✔
314
        dot.node(
1✔
315
            fixed_width_text(label, char_num=self.txt_width),
316
            shape="trapezium",
317
            fontsize=self.txt_fontsize,
318
        )
319

320
    def add_source(self, label="Source", subgraph=None):
1✔
321
        if subgraph is None:
1✔
322
            dot = self.dot
1✔
323
        else:
324
            dot = subgraph
1✔
325
        dot.node(
1✔
326
            fixed_width_text(label, char_num=self.txt_width),
327
            shape="invtrapezium",
328
            fontsize=self.txt_fontsize,
329
        )
330

331
    def add_transformer(self, label="Transformer", subgraph=None):
1✔
332
        if subgraph is None:
1✔
333
            dot = self.dot
1✔
334
        else:
335
            dot = subgraph
1✔
336
        dot.node(
1✔
337
            fixed_width_text(label, char_num=self.txt_width),
338
            shape="rectangle",
339
            fontsize=self.txt_fontsize,
340
        )
341

342
    def add_storage(self, label="Storage", subgraph=None):
1✔
343
        if subgraph is None:
1✔
344
            dot = self.dot
1✔
345
        else:
346
            dot = subgraph
1✔
347
        dot.node(
1✔
348
            fixed_width_text(label, char_num=self.txt_width),
349
            shape="rectangle",
350
            style="rounded",
351
            fontsize=self.txt_fontsize,
352
        )
353

354
    def add_component(self, label="component", subgraph=None):
1✔
355
        if subgraph is None:
×
356
            dot = self.dot
×
357
        else:
358
            dot = subgraph
×
359
        dot.node(
×
360
            fixed_width_text(label, char_num=self.txt_width),
361
            fontsize=self.txt_fontsize,
362
        )
363

364
    def connect(self, a, b):
1✔
365
        """Draw an arrow from node a to node b
366

367
        Parameters
368
        ----------
369
        a: `oemof.solph.network.Node`
370
            An oemof node (usually a Bus or a Component)
371

372
        b: `oemof.solph.network.Node`
373
            An oemof node (usually a Bus or a Component)
374
        """
375
        if not isinstance(a, oemof.network.Bus):
1✔
376
            a = fixed_width_text(a.label, char_num=self.txt_width)
1✔
377
        else:
378
            a = a.label
1✔
379
        if not isinstance(b, oemof.network.Bus):
1✔
380
            b = fixed_width_text(b.label, char_num=self.txt_width)
1✔
381
        else:
382
            b = b.label
1✔
383

384
        self.dot.edge(a, b)
1✔
385

386
    def view(self, **kwargs):
1✔
387
        """Call the view method of the DiGraph instance"""
388
        self.dot.view(**kwargs)
×
389

390
    def render(self, **kwargs):
1✔
391
        """Call the render method of the DiGraph instance"""
392
        self.dot.render(**kwargs)
1✔
393

394
    def sankey(self, results):
1✔
395
        """Return a dict to a plotly sankey diagram"""
396
        busses = []
×
397

398
        labels = []
×
399
        sources = []
×
400
        targets = []
×
401
        values = []
×
402

403
        # bus_data.update({bus: solph.views.node(results_main, bus)})
404

405
        # draw a node for each of the network's component. The shape depends on the component's type
406
        for nd in self.energy_system.nodes:
×
407
            if isinstance(nd, oemof.network.Bus):
×
408

409
                # keep the bus reference for drawing edges later
410
                bus = nd
×
411
                busses.append(bus)
×
412

413
                bus_label = bus.label
×
414

415
                labels.append(nd.label)
×
416

417
                flows = solph.views.node(results, bus_label)["sequences"]
×
418

419
                # draw an arrow from the component to the bus
420
                for component in bus.inputs:
×
421
                    if component.label not in labels:
×
422
                        labels.append(component.label)
×
423

424
                    sources.append(labels.index(component.label))
×
425
                    targets.append(labels.index(bus_label))
×
426

427
                    val = flows[((component.label, bus_label), "flow")].sum()
×
428
                    # if val == 0:
429
                    #     val = 1
430
                    values.append(val)
×
431

432
                for component in bus.outputs:
×
433
                    # draw an arrow from the bus to the component
434
                    if component.label not in labels:
×
435
                        labels.append(component.label)
×
436

437
                    sources.append(labels.index(bus_label))
×
438
                    targets.append(labels.index(component.label))
×
439

440
                    val = flows[((bus_label, component.label), "flow")].sum()
×
441

442
                    # if val == 0:
443
                    #     val = 1
444
                    values.append(val)
×
445

446
        fig = go.Figure(
×
447
            data=[
448
                go.Sankey(
449
                    node=dict(
450
                        pad=15,
451
                        thickness=20,
452
                        line=dict(color="black", width=0.5),
453
                        label=labels,
454
                        hovertemplate="Node has total value %{value}<extra></extra>",
455
                        color="blue",
456
                    ),
457
                    link=dict(
458
                        source=sources,  # indices correspond to labels, eg A1, A2, A2, B1, ...
459
                        target=targets,
460
                        value=values,
461
                        hovertemplate="Link from node %{source.label}<br />"
462
                        + "to node%{target.label}<br />has value %{value}"
463
                        + "<br />and data <extra></extra>",
464
                    ),
465
                )
466
            ]
467
        )
468

469
        fig.update_layout(title_text="Basic Sankey Diagram", font_size=10)
×
470
        return fig.to_dict()
×
471

472

473
def get_color(idx_line, color_list=None):
1✔
474
    """Pick a color within a color list with periodic boundary conditions
475

476
    Parameters
477
    ----------
478
    idx_line: int
479
        index of the line in a plot for which a color is required
480

481
    colors: list of str or list to tuple (hexadecimal or rbg code)
482
        list of colors
483
        Default: None
484

485
    Returns
486
    -------
487
    The color in the color list corresponding to the index modulo the color list length
488

489
    """
490
    if color_list is None:
1✔
491
        color_list = (
×
492
            "#1f77b4",
493
            "#ff7f0e",
494
            "#2ca02c",
495
            "#d62728",
496
            "#9467bd",
497
            "#8c564b",
498
            "#e377c2",
499
            "#7f7f7f",
500
            "#bcbd22",
501
            "#17becf",
502
        )
503
    n_colors = len(color_list)
1✔
504
    return color_list[idx_line % n_colors]
1✔
505

506

507
def save_plots_to_disk(
1✔
508
    fig_obj, file_name, file_path="", width=None, height=None, scale=None
509
):
510
    r"""
511
    This function saves the plots generated using the Plotly library in this module to the outputs folder.
512

513
    Parameters
514
    ----------
515
    fig_obj: instance of the classes of the Plotly go library used to generate the plots in this auto-report
516
        Figure object of the plotly plots
517

518
    file_name: str
519
        The name of the PNG image of the plot to be saved in the output folder.
520

521
    file_path: str
522
        Path where the image shall be saved
523

524
    width: int or float
525
        The width of the picture to be saved in pixels.
526
        Default: None
527

528
    height: int or float
529
        The height of the picture to be saved in pixels.
530
        Default: None
531

532
    scale: int or float
533
        The scale by which the plotly image ought to be multiplied.
534
        Default: None
535

536
    Returns
537
    -------
538
    Nothing is returned. This function call results in the plots being saved as .png images to the disk.
539
    """
540

541
    if not file_name.endswith("png"):
1✔
542
        file_name = file_name + ".png"
×
543

544
    logging.info("Saving {} under {}".format(file_name, file_path))
1✔
545

546
    file_path_out = os.path.join(file_path, file_name)
1✔
547
    with open(file_path_out, "wb") as fp:
1✔
548
        fig_obj.write_image(fp, width=width, height=height, scale=scale)
1✔
549

550

551
def get_fig_style_dict():
1✔
552
    styling_dict = dict(
×
553
        showgrid=True,
554
        gridwidth=1.5,
555
        zeroline=True,
556
        autorange=True,
557
        linewidth=1,
558
        ticks="inside",
559
        title_font=dict(size=18, color="black"),
560
    )
561
    return styling_dict
×
562

563

564
def create_plotly_line_fig(
1✔
565
    x_data,
566
    y_data,
567
    plot_title=None,
568
    x_axis_name=None,
569
    y_axis_name=None,
570
    color_for_plot="#0A2342",
571
    file_path=None,
572
):
573
    r"""
574
    Create figure for generic timeseries lineplots
575

576
    Parameters
577
    ----------
578
    x_data: list, or pandas series
579
        The list of abscissas of the data required for plotting.
580

581
    y_data: list, or pandas series, or list of lists
582
        The list of ordinates of the data required for plotting.
583

584
    plot_title: str
585
        The title of the plot generated.
586
        Default: None
587

588
    x_axis_name: str
589
        Default: None
590

591
    y_axis_name: str
592
        Default: None
593

594
    file_path: str
595
        Path where the image shall be saved if not None
596

597
    Returns
598
    -------
599
    fig :class:`plotly.graph_objs.Figure`
600
        figure object
601
    """
602
    fig = go.Figure()
×
603

604
    styling_dict = get_fig_style_dict()
×
605
    styling_dict["mirror"] = True
×
606

607
    fig.add_trace(
×
608
        go.Scatter(
609
            x=x_data,
610
            y=y_data,
611
            mode="lines",
612
            line=dict(color=color_for_plot, width=2.5),
613
        )
614
    )
615
    fig.update_layout(
×
616
        xaxis_title=x_axis_name,
617
        yaxis_title=y_axis_name,
618
        template="simple_white",
619
        xaxis=styling_dict,
620
        yaxis=styling_dict,
621
        font_family="sans-serif",
622
        title={
623
            "text": plot_title,
624
            "y": 0.90,
625
            "x": 0.5,
626
            "font_size": 23,
627
            "xanchor": "center",
628
            "yanchor": "top",
629
        },
630
    )
631

632
    name_file = "input_timeseries_" + plot_title + ".png"
×
633

634
    if file_path is not None:
×
635

636
        # Function call to save the Plotly plot to the disk
637
        save_plots_to_disk(
×
638
            fig_obj=fig,
639
            file_path=file_path,
640
            file_name=name_file,
641
            width=1200,
642
            height=600,
643
            scale=5,
644
        )
645

646
    return fig
×
647

648

649
def plot_timeseries(
1✔
650
    dict_values,
651
    data_type=DEMANDS,
652
    sector_demands=None,
653
    max_days=None,
654
    color_list=None,
655
    file_path=None,
656
):
657
    r"""Plot timeseries as line chart.
658

659
    Parameters
660
    ----------
661
    dict_values :
662
        dict Of all input and output parameters up to F0
663

664
    data_type: str
665
        one of DEMANDS or RESOURCES
666
        Default: DEMANDS
667

668
    sector_demands: str
669
        Name of the sector of the energy system
670
        Default: None
671

672
    max_days: int
673
        maximal number of days the timeseries should be displayed for
674

675
    color_list: list of str or list to tuple (hexadecimal or rbg code)
676
        list of colors
677
        Default: None
678

679
    file_path: str
680
        Path where the image shall be saved if not None
681
        Default: None
682

683
    Returns
684
    -------
685
    Dict with html DOM id for the figure as key and :class:`plotly.graph_objs.Figure` as value
686
    """
687

688
    df_dem = convert_demand_to_dataframe(
×
689
        dict_values=dict_values, sector_demands=sector_demands
690
    )
691
    dict_for_plots, dict_plot_labels = extract_plot_data_and_title(
×
692
        dict_values, df_dem=df_dem
693
    )
694

695
    df_pd = convert_plot_data_to_dataframe(dict_for_plots, data_type)
×
696

697
    list_of_keys = list(df_pd.columns)
×
698
    list_of_keys.remove("timestamp")
×
699
    plots = {}
×
700

701
    if max_days is not None:
×
702
        if df_pd["timestamp"].empty:
×
703
            logging.warning("The timeseries for {} are empty".format(data_type))
×
UNCOV
704
        elif df_pd.timestamp.dtype == np.int64:
×
705
            logging.warning(
×
706
                "The timeseries for {} do not have correct timestamps, it is likely that you uploaded "
707
                "a timeseries with more or less values than the number of days multiplied by number of "
708
                "timesteps within a day.".format(data_type)
709
            )
710
        else:
UNCOV
711
            if not isinstance(df_pd["timestamp"], pd.DatetimeIndex):
×
712
                dti = dict_values[SIMULATION_SETTINGS][TIME_INDEX]
×
713
                df_pd = df_pd.loc[: len(dti) - 1]
×
UNCOV
714
                df_pd["timestamp"] = dti
×
715
                max_date = df_pd["timestamp"][0] + pd.Timedelta(
×
716
                    "{} day".format(max_days)
717
                )
718
            df_pd = df_pd.loc[df_pd["timestamp"] < max_date]
×
719
        title_addendum = " ({} days)".format(max_days)
×
720
    else:
UNCOV
721
        title_addendum = ""
×
722

UNCOV
723
    for i, component in enumerate(list_of_keys):
×
UNCOV
724
        comp_id = component + "-plot"
×
UNCOV
725
        fig = create_plotly_line_fig(
×
726
            x_data=df_pd["timestamp"],
727
            y_data=df_pd[component],
728
            plot_title="{}{}".format(dict_plot_labels[component], title_addendum),
729
            x_axis_name="Time",
730
            y_axis_name="kW",
731
            color_for_plot=get_color(i, color_list),
732
            file_path=file_path,
733
        )
UNCOV
734
        if file_path is None:
×
UNCOV
735
            plots[comp_id] = fig
×
736

737
    return plots
×
738

739

740
def plot_sankey(dict_values):
1✔
741
    """"""
UNCOV
742
    fig_dict = dict_values[PATHS_TO_PLOTS].get(PLOT_SANKEY, None)
×
UNCOV
743
    if fig_dict is not None:
×
UNCOV
744
        fig = go.Figure(**fig_dict)
×
745
    else:
UNCOV
746
        fig = go.Figure()
×
UNCOV
747
    return fig
×
748

749

750
def create_plotly_barplot_fig(
1✔
751
    x_data,
752
    y_data,
753
    plot_title=None,
754
    trace_name="",
755
    legends=None,
756
    x_axis_name=None,
757
    y_axis_name=None,
758
    file_name="barplot.png",
759
    file_path=None,
760
):
761
    r"""
762
    Create figure for specific capacities barplot
763

764
    Parameters
765
    ----------
766
    x_data: list, or pandas series
767
        The list of abscissas of the data required for plotting.
768

769
    y_data: list, or pandas series, or list of lists
770
        The list of ordinates of the data required for plotting.
771

772
    plot_title: str
773
        The title of the plot generated.
774
        Default: None
775

776
    trace_name: str
777
        Sets the trace name. The trace name appear as the legend item and on hover.
778
        Default: ""
779

780
    legends: list, or pandas series
781
        The list of the text written within the bars and on hover below the trace_name
782
        Default: None
783

784
    x_axis_name: str
785
        Default: None
786

787
    y_axis_name: str
788
        Default: None
789

790
    file_name: str
791
        Name of the image file.
792
        Default: "barplot.png"
793

794
    file_path: str
795
        Path where the image shall be saved if not None
796

797
    Returns
798
    -------
799
    fig: :class:`plotly.graph_objs.Figure`
800
        figure object
801
    """
802
    fig = go.Figure()
×
803

UNCOV
804
    styling_dict = get_fig_style_dict()
×
805
    styling_dict["mirror"] = True
×
806

UNCOV
807
    opts = {}
×
UNCOV
808
    if legends is not None:
×
UNCOV
809
        opts.update(dict(text=legends, textposition="auto"))
×
810

UNCOV
811
    fig.add_trace(
×
812
        go.Bar(
813
            name=trace_name,
814
            x=x_data,
815
            y=y_data,
816
            marker_color=px.colors.qualitative.D3,
817
            **opts,
818
        )
819
    )
820

UNCOV
821
    fig.update_layout(
×
822
        xaxis_title=x_axis_name,
823
        yaxis_title=y_axis_name,
824
        template="simple_white",
825
        font_family="sans-serif",
826
        # TODO use styling dict
827
        xaxis=go.layout.XAxis(
828
            showgrid=True,
829
            gridwidth=1.5,
830
            zeroline=True,
831
            mirror=True,
832
            autorange=True,
833
            linewidth=1,
834
            ticks="inside",
835
            visible=True,
836
        ),
837
        yaxis=styling_dict,
838
        title={
839
            "text": plot_title,
840
            "y": 0.90,
841
            "x": 0.5,
842
            "font_size": 23,
843
            "xanchor": "center",
844
            "yanchor": "top",
845
        },
846
        legend_title="Components",
847
    )
848

UNCOV
849
    if file_path is not None:
×
850
        # Function call to save the Plotly plot to the disk
UNCOV
851
        save_plots_to_disk(
×
852
            fig_obj=fig,
853
            file_path=file_path,
854
            file_name=file_name,
855
            width=1200,
856
            height=600,
857
            scale=5,
858
        )
859

UNCOV
860
    return fig
×
861

862

863
def plot_optimized_capacities(
1✔
864
    dict_values,
865
    file_path=None,
866
):
867
    """Plot capacities as a bar chart.
868

869
    Parameters
870
    ----------
871
    dict_values :
872
        dict Of all input and output parameters up to F0
873

874
    file_path: str
875
        Path where the image shall be saved if not None
876
        Default: None
877

878
    Returns
879
    -------
880
    Dict with html DOM id for the figure as key and :class:`plotly.graph_objs.Figure` as value
881
    """
882

883
    # Add dataframe to hold all the KPIs and optimized additional capacities
884
    df_capacities = dict_values[KPI][KPI_SCALAR_MATRIX].copy(deep=True)
×
885
    df_capacities.drop(
×
886
        columns=[TOTAL_FLOW, ANNUAL_TOTAL_FLOW, PEAK_FLOW, AVERAGE_FLOW],
887
        inplace=True,
888
    )
889
    df_capacities.reset_index(drop=True, inplace=True)
×
890

UNCOV
891
    x_values = []
×
UNCOV
892
    y_values = []
×
UNCOV
893
    legends = []
×
894

895
    for kpi, cap, unit in zip(
×
896
        list(df_capacities[LABEL]),
897
        list(df_capacities[OPTIMIZED_ADD_CAP]),
898
        list(df_capacities[UNIT]),
899
    ):
UNCOV
900
        if cap > 0:
×
UNCOV
901
            x_values.append(kpi)
×
902
            y_values.append(cap)
×
UNCOV
903
            if unit == "?":
×
UNCOV
904
                unit = "kW"
×
UNCOV
905
            legends.append("{:.0f} {}".format(cap, unit))
×
906

907
    # Title to add to plot titles
UNCOV
908
    project_title = ": {}, {}".format(
×
909
        dict_values[PROJECT_DATA][PROJECT_NAME],
910
        dict_values[PROJECT_DATA][SCENARIO_NAME],
911
    )
912

UNCOV
913
    name_file = "optimal_additional_capacities"
×
914

UNCOV
915
    fig = create_plotly_barplot_fig(
×
916
        x_data=x_values,
917
        y_data=y_values,
918
        plot_title="Optimal additional capacities" + project_title,
919
        trace_name="capacities",
920
        legends=legends,
921
        x_axis_name="Items",
922
        y_axis_name="Capacities",
923
        file_name=name_file,
924
        file_path=file_path,
925
    )
926

UNCOV
927
    return {"capacities_plot": fig}
×
928

929

930
def create_plotly_flow_fig(
1✔
931
    df_plots_data,
932
    x_legend=None,
933
    y_legend=None,
934
    plot_title=None,
935
    color_list=None,
936
    file_name="flows.png",
937
    file_path=None,
938
):
939
    r"""Generate figure of an asset's flow.
940

941
    Parameters
942
    ----------
943
    df_plots_data: :class:`pandas.DataFrame<frame>`
944
        dataFrame with timeseries of the asset's energy flow
945
    x_legend: str
946
        Default: None
947

948
    y_legend: str
949
        Default: None
950

951
    plot_title: str
952
        Default: None
953

954
    color_list: list of str or list to tuple (hexadecimal or rbg code)
955
        list of colors
956
        Default: None
957

958
    file_name: str
959
        Name of the image file.
960
        Default: "flows.png"
961

962
    file_path: str
963
        Path where the image shall be saved if not None
964
        Default: None
965

966
    Returns
967
    -------
968
    fig: :class:`plotly.graph_objs.Figure`
969
        figure object
970
    """
971

UNCOV
972
    fig = go.Figure()
×
973
    styling_dict = get_fig_style_dict()
×
974
    styling_dict["gridwidth"] = 1.0
×
975

UNCOV
976
    assets_list = list(df_plots_data.columns)
×
UNCOV
977
    assets_list.remove("timestamp")
×
978

UNCOV
979
    for i, asset in enumerate(assets_list):
×
UNCOV
980
        fig.add_trace(
×
981
            go.Scatter(
982
                x=df_plots_data["timestamp"],
983
                y=df_plots_data[asset],
984
                mode="lines",
985
                line=dict(color=get_color(i, color_list), width=2.5),
986
                name=asset,
987
            )
988
        )
989

UNCOV
990
    fig.update_layout(
×
991
        xaxis_title=x_legend,
992
        yaxis_title=y_legend,
993
        font_family="sans-serif",
994
        template="simple_white",
995
        xaxis=styling_dict,
996
        yaxis=styling_dict,
997
        title={
998
            "text": plot_title,
999
            "y": 0.90,
1000
            "x": 0.5,
1001
            "font_size": 23,
1002
            "xanchor": "center",
1003
            "yanchor": "top",
1004
        },
1005
        legend=dict(
1006
            y=0.5,
1007
            traceorder="normal",
1008
            font=dict(color="black"),
1009
        ),
1010
    )
1011

UNCOV
1012
    if file_path is not None:
×
1013
        # Function call to save the Plotly plot to the disk
UNCOV
1014
        save_plots_to_disk(
×
1015
            fig_obj=fig,
1016
            file_path=file_path,
1017
            file_name=file_name,
1018
            width=1200,
1019
            height=600,
1020
            scale=5,
1021
        )
1022

UNCOV
1023
    return fig
×
1024

1025

1026
def plot_instant_power(dict_values, file_path=None):
1✔
1027
    """Plotting timeseries of instantaneous power for each assets within the energy system
1028

1029
    Parameters
1030
    ----------
1031
    dict_values : dict
1032
        all simulation input and output data up to this point
1033

1034
    file_path: str
1035
        Path where the image shall be saved if not None
1036
        Default: None
1037

1038
    Returns
1039
    -------
1040
    multi_plots: dict
1041
       Dict with html DOM id for the figure as keys and :class:`plotly.graph_objs.Figure` as values
1042
    """
UNCOV
1043
    buses_list = list(dict_values[OPTIMIZED_FLOWS].keys())
×
UNCOV
1044
    multi_plots = {}
×
UNCOV
1045
    for bus in buses_list:
×
UNCOV
1046
        df_data = dict_values[OPTIMIZED_FLOWS][bus].copy(deep=True)
×
1047
        df_data.reset_index(level=0, inplace=True)
×
UNCOV
1048
        df_data = df_data.rename(columns={"index": "timestamp"})
×
1049

1050
        # In case SOC of a storage is in df_data the SOC is plotted separately and is
1051
        # removed from the df_data as the plot that shows absolute flows should not
1052
        # contain SOC in %
UNCOV
1053
        if any(SOC in item for item in df_data):
×
1054
            # if any(SOC in item for item in df_data):
UNCOV
1055
            comp_id = f"{SOC}-{bus}-plot"
×
UNCOV
1056
            title = (
×
1057
                bus
1058
                + " storage SOC in LES: "
1059
                + dict_values[PROJECT_DATA][PROJECT_NAME]
1060
                + ", "
1061
                + dict_values[PROJECT_DATA][SCENARIO_NAME]
1062
            )
1063
            # get columns containing SOC and plot SOC
UNCOV
1064
            soc_cols = [s for s in df_data.keys() if SOC in s]
×
UNCOV
1065
            soc_cols.extend(["timestamp"])
×
UNCOV
1066
            fig = create_plotly_flow_fig(
×
1067
                df_plots_data=df_data[soc_cols],
1068
                x_legend="Time",
1069
                y_legend="SOC",
1070
                plot_title=title,
1071
                file_path=file_path,
1072
                file_name=f"SOC_{bus}_power.png",
1073
            )
UNCOV
1074
            if file_path is None:
×
UNCOV
1075
                multi_plots[comp_id] = fig
×
1076

1077
            # remove SOC as it is provided in % and does not fit with flow plot
UNCOV
1078
            soc_cols.remove("timestamp")
×
UNCOV
1079
            df_data.drop(soc_cols, inplace=True, axis=1)
×
1080

1081
        # create flow plot
UNCOV
1082
        comp_id = f"{bus}-plot"
×
UNCOV
1083
        title = (
×
1084
            bus
1085
            + " power in LES: "
1086
            + dict_values[PROJECT_DATA][PROJECT_NAME]
1087
            + ", "
1088
            + dict_values[PROJECT_DATA][SCENARIO_NAME]
1089
        )
1090

UNCOV
1091
        fig = create_plotly_flow_fig(
×
1092
            df_plots_data=df_data,
1093
            x_legend="Time",
1094
            y_legend=bus + " in kW",
1095
            plot_title=title,
1096
            file_path=file_path,
1097
            file_name=bus + "_power.png",
1098
        )
UNCOV
1099
        if file_path is None:
×
UNCOV
1100
            multi_plots[comp_id] = fig
×
1101

UNCOV
1102
    return multi_plots
×
1103

1104

1105
def create_plotly_piechart_fig(
1✔
1106
    title_of_plot,
1107
    names,
1108
    values,
1109
    color_scheme=None,
1110
    file_name="costs.png",
1111
    file_path=None,
1112
):
1113
    r"""Generate figure with piechart plot.
1114

1115
    Parameters
1116
    ----------
1117
    title_of_plot: str
1118
        title of the figure
1119

1120
    names: list
1121
        List containing the labels of the slices in the pie plot.
1122

1123
    values: list
1124
        List containing the values of the labels to be plotted in the pie plot.
1125

1126
    color_scheme: instance of the px.colors class of the Plotly express library
1127
        This parameter holds the color scheme which is palette of colors (list of hex values) to be
1128
        applied to the pie plot to be created.
1129
        Default: None
1130

1131
    file_name: str
1132
        Name of the image file.
1133
        Default: "costs.png"
1134

1135
    file_path: str
1136
        Path where the image shall be saved if not None
1137
        Default: None
1138

1139
    Returns
1140
    -------
1141
    fig: :class:`plotly.graph_objs.Figure`
1142
        figure object
1143
    """
1144

1145
    if color_scheme is None:
1✔
1146
        color_scheme = px.colors.qualitative.Set1
1✔
1147

1148
    # Wrap the text of the title into next line if it exceeds the length given below
1149
    title_of_plot = textwrap.wrap(title_of_plot, width=75)
1✔
1150
    title_of_plot = "<br>".join(title_of_plot)
1✔
1151

1152
    fig = go.Figure(
1✔
1153
        go.Pie(
1154
            labels=names,
1155
            values=values,
1156
            textposition="inside",
1157
            insidetextorientation="radial",
1158
            texttemplate="%{label} <br>%{percent}",
1159
            marker=dict(colors=color_scheme),
1160
        ),
1161
    )
1162

1163
    fig.update_layout(
1✔
1164
        title={
1165
            "text": title_of_plot,
1166
            "y": 0.9,
1167
            "x": 0.5,
1168
            "font_size": 23,
1169
            "xanchor": "center",
1170
            "yanchor": "top",
1171
            "pad": {"r": 5, "l": 5, "b": 5, "t": 5},
1172
        },
1173
        font_family="sans-serif",
1174
        height=500,
1175
        width=700,
1176
        autosize=True,
1177
        legend=dict(
1178
            orientation="v",
1179
            y=0.5,
1180
            yanchor="middle",
1181
            x=0.95,
1182
            xanchor="right",
1183
        ),
1184
        margin=dict(l=10, r=10, b=50, pad=2),
1185
        uniformtext_minsize=18,
1186
    )
1187
    fig.update_traces(hoverinfo="label+percent", textinfo="label", textfont_size=18)
1✔
1188

1189
    if file_path is not None:
1✔
1190
        # Function call to save the Plotly plot to the disk
1191
        save_plots_to_disk(
1✔
1192
            fig_obj=fig,
1193
            file_path=file_path,
1194
            file_name=file_name,
1195
            width=1200,
1196
            height=600,
1197
            scale=5,
1198
        )
1199

1200
    return fig
1✔
1201

1202

1203
def plot_piecharts_of_costs(dict_values, file_path=None):
1✔
1204
    """Plotting piecharts of different cost parameters (ie. annuity, total cost, etc...)
1205

1206
    Parameters
1207
    ----------
1208
    dict_values : dict
1209
        all simulation input and output data up to this point
1210

1211
    file_path: str
1212
        Path where the image shall be saved if not None
1213
        Default: None
1214

1215
    Returns
1216
    -------
1217
    pie_plots: dict
1218
       Dict with html DOM id for the figure as keys and :class:`plotly.graph_objs.Figure` as values
1219
    """
1220

UNCOV
1221
    df_pie_data = convert_costs_to_dataframe(dict_values)
×
1222

1223
    # Initialize an empty list and a dict for use later in the function
UNCOV
1224
    pie_plots = {}
×
UNCOV
1225
    pie_data_dict = {}
×
1226

1227
    # df_pie_data.reset_index(drop=True, inplace=True)
UNCOV
1228
    columns_list = list(df_pie_data.columns)
×
1229
    columns_list.remove(LABEL)
×
1230

1231
    # Iterate through the list of columns of the DF which are the KPIs to be plotted
UNCOV
1232
    for kp_indic in columns_list:
×
1233

1234
        # Assign an id for the plot
UNCOV
1235
        comp_id = kp_indic + "plot"
×
1236

1237
        kpi_part = ""
×
1238

1239
        # Make a copy of the DF to make various manipulations for the pie chart plotting
UNCOV
1240
        df_temp = df_pie_data.copy()
×
1241

1242
        # Get the total value for each KPI to use in the title of the respective pie chart
UNCOV
1243
        df_temp2 = df_temp.copy()
×
1244
        df_temp2.set_index(LABEL, inplace=True)
×
1245
        total_for_title = df_temp2.at["Total", kp_indic]
×
1246

1247
        # Drop the total row in the dataframe
UNCOV
1248
        df_temp.drop(df_temp.tail(1).index, inplace=True)
×
1249
        # Gather the data for each asset for the particular KPI, in a dict
1250
        for row_index in df_temp.index:
×
UNCOV
1251
            pie_data_dict[df_temp.at[row_index, LABEL]] = df_temp.at[
×
1252
                row_index, kp_indic
1253
            ]
1254

1255
        # Remove negative values (such as the feed-in sinks) from the dict
UNCOV
1256
        pie_data_dict = {k: v for (k, v) in pie_data_dict.items() if v > 0}
×
1257

1258
        # Get the names and values for the pie chart from the above dict
1259
        names_plot = list(pie_data_dict.keys())
×
1260
        values_plot = list(pie_data_dict.values())
×
1261

1262
        # Below loop determines the first part of the plot title, according to the kpi being plotted
1263
        if "annuity" in kp_indic:
×
1264
            kpi_part = "Annuity Costs ("
×
1265
            file_name = "annuity"
×
1266
            scheme_choosen = px.colors.qualitative.Set1
×
1267
        elif "investment" in kp_indic:
×
1268
            kpi_part = "Upfront Investment Costs ("
×
UNCOV
1269
            file_name = "upfront_investment_costs"
×
UNCOV
1270
            scheme_choosen = px.colors.diverging.BrBG
×
1271
        elif "om" in kp_indic:
×
UNCOV
1272
            kpi_part = "Operation and Maintenance Costs ("
×
UNCOV
1273
            file_name = "operation_and_maintainance_costs"
×
UNCOV
1274
            scheme_choosen = px.colors.sequential.RdBu
×
1275

1276
        # Title to add to plot titles
1277
        project_title = ": {}, {}".format(
×
1278
            dict_values[PROJECT_DATA][PROJECT_NAME],
1279
            dict_values[PROJECT_DATA][SCENARIO_NAME],
1280
        )
1281

1282
        # Title of the pie plot
UNCOV
1283
        plot_title = (
×
1284
            kpi_part
1285
            + str(round(total_for_title))
1286
            + " "
1287
            + dict_values[ECONOMIC_DATA][CURR]
1288
            + ") "
1289
            + project_title
1290
        )
1291

UNCOV
1292
        fig = create_plotly_piechart_fig(
×
1293
            title_of_plot=plot_title,
1294
            names=names_plot,
1295
            values=values_plot,
1296
            color_scheme=scheme_choosen,
1297
            file_name=file_name,
1298
            file_path=file_path,
1299
        )
1300

UNCOV
1301
        if file_path is None:
×
UNCOV
1302
            pie_plots[comp_id] = fig
×
1303

UNCOV
1304
    return pie_plots
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc