generator_plots.py revision af940b46
1# Copyright (c) 2019 Cisco and/or its affiliates.
2# Licensed under the Apache License, Version 2.0 (the "License");
3# you may not use this file except in compliance with the License.
4# You may obtain a copy of the License at:
5#
6#     http://www.apache.org/licenses/LICENSE-2.0
7#
8# Unless required by applicable law or agreed to in writing, software
9# distributed under the License is distributed on an "AS IS" BASIS,
10# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11# See the License for the specific language governing permissions and
12# limitations under the License.
13
14"""Algorithms to generate plots.
15"""
16
17
18import re
19import logging
20
21from collections import OrderedDict
22from copy import deepcopy
23
24import hdrh.histogram
25import hdrh.codec
26import pandas as pd
27import plotly.offline as ploff
28import plotly.graph_objs as plgo
29
30from plotly.subplots import make_subplots
31from plotly.exceptions import PlotlyError
32
33from pal_utils import mean, stdev
34
35
36COLORS = [u"SkyBlue", u"Olive", u"Purple", u"Coral", u"Indigo", u"Pink",
37          u"Chocolate", u"Brown", u"Magenta", u"Cyan", u"Orange", u"Black",
38          u"Violet", u"Blue", u"Yellow", u"BurlyWood", u"CadetBlue", u"Crimson",
39          u"DarkBlue", u"DarkCyan", u"DarkGreen", u"Green", u"GoldenRod",
40          u"LightGreen", u"LightSeaGreen", u"LightSkyBlue", u"Maroon",
41          u"MediumSeaGreen", u"SeaGreen", u"LightSlateGrey"]
42
43REGEX_NIC = re.compile(r'\d*ge\dp\d\D*\d*-')
44
45
46def generate_plots(spec, data):
47    """Generate all plots specified in the specification file.
48
49    :param spec: Specification read from the specification file.
50    :param data: Data to process.
51    :type spec: Specification
52    :type data: InputData
53    """
54
55    generator = {
56        u"plot_nf_reconf_box_name": plot_nf_reconf_box_name,
57        u"plot_perf_box_name": plot_perf_box_name,
58        u"plot_lat_err_bars_name": plot_lat_err_bars_name,
59        u"plot_tsa_name": plot_tsa_name,
60        u"plot_http_server_perf_box": plot_http_server_perf_box,
61        u"plot_nf_heatmap": plot_nf_heatmap,
62        u"plot_lat_hdrh_bar_name": plot_lat_hdrh_bar_name,
63        u"plot_lat_hdrh_percentile": plot_lat_hdrh_percentile
64    }
65
66    logging.info(u"Generating the plots ...")
67    for index, plot in enumerate(spec.plots):
68        try:
69            logging.info(f"  Plot nr {index + 1}: {plot.get(u'title', u'')}")
70            plot[u"limits"] = spec.configuration[u"limits"]
71            generator[plot[u"algorithm"]](plot, data)
72            logging.info(u"  Done.")
73        except NameError as err:
74            logging.error(
75                f"Probably algorithm {plot[u'algorithm']} is not defined: "
76                f"{repr(err)}"
77            )
78    logging.info(u"Done.")
79
80
81def plot_lat_hdrh_percentile(plot, input_data):
82    """Generate the plot(s) with algorithm: plot_lat_hdrh_percentile
83    specified in the specification file.
84
85    :param plot: Plot to generate.
86    :param input_data: Data to process.
87    :type plot: pandas.Series
88    :type input_data: InputData
89    """
90
91    # Transform the data
92    plot_title = plot.get(u"title", u"")
93    logging.info(
94        f"    Creating the data set for the {plot.get(u'type', u'')} "
95        f"{plot_title}."
96    )
97    data = input_data.filter_tests_by_name(
98        plot, params=[u"latency", u"parent", u"tags", u"type"])
99    if data is None or len(data[0][0]) == 0:
100        logging.error(u"No data.")
101        return
102
103    fig = plgo.Figure()
104
105    # Prepare the data for the plot
106    directions = [u"W-E", u"E-W"]
107    for test in data[0][0]:
108        try:
109            if test[u"type"] in (u"NDRPDR",):
110                if u"-pdr" in plot_title.lower():
111                    ttype = u"PDR"
112                elif u"-ndr" in plot_title.lower():
113                    ttype = u"NDR"
114                else:
115                    logging.warning(f"Invalid test type: {test[u'type']}")
116                    continue
117                name = re.sub(REGEX_NIC, u"", test[u"parent"].
118                              replace(u'-ndrpdr', u'').
119                              replace(u'2n1l-', u'').
120                              replace(u'avf-', u''))
121                for idx, direction in enumerate(
122                        (u"direction1", u"direction2", )):
123                    try:
124                        hdr_lat = test[u"latency"][ttype][direction][u"hdrh"]
125                        # TODO: Workaround, HDRH data must be aligned to 4
126                        #       bytes, remove when not needed.
127                        hdr_lat += u"=" * (len(hdr_lat) % 4)
128                        xaxis = list()
129                        yaxis = list()
130                        hovertext = list()
131                        decoded = hdrh.histogram.HdrHistogram.decode(hdr_lat)
132                        for item in decoded.get_recorded_iterator():
133                            percentile = item.percentile_level_iterated_to
134                            if percentile != 100.0:
135                                xaxis.append(100.0 / (100.0 - percentile))
136                                yaxis.append(item.value_iterated_to)
137                                hovertext.append(
138                                    f"Test: {name}<br>"
139                                    f"Direction: {directions[idx]}<br>"
140                                    f"Percentile: {percentile:.5f}<br>%"
141                                    f"Latency: {item.value_iterated_to}uSec"
142                                )
143                        fig.add_trace(
144                            plgo.Scatter(
145                                x=xaxis,
146                                y=yaxis,
147                                name=name,
148                                mode=u"lines",
149                                hovertext=hovertext,
150                                hoverinfo=u"text"
151                            )
152                        )
153                    except hdrh.codec.HdrLengthException as err:
154                        logging.warning(
155                            f"No or invalid data for HDRHistogram for the test "
156                            f"{name}\n{err}"
157                        )
158                        continue
159            else:
160                logging.warning(f"Invalid test type: {test[u'type']}")
161                continue
162        except (ValueError, KeyError) as err:
163            logging.warning(repr(err))
164
165    layout = deepcopy(plot[u"layout"])
166
167    layout[u"title"][u"text"] = \
168        f"<b>Latency:</b> {plot.get(u'graph-title', u'')}"
169    fig[u"layout"].update(layout)
170
171    # Create plot
172    file_type = plot.get(u"output-file-type", u".html")
173    logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
174    try:
175        # Export Plot
176        ploff.plot(fig, show_link=False, auto_open=False,
177                   filename=f"{plot[u'output-file']}{file_type}")
178    except PlotlyError as err:
179        logging.error(f"   Finished with error: {repr(err)}")
180
181
182def plot_lat_hdrh_bar_name(plot, input_data):
183    """Generate the plot(s) with algorithm: plot_lat_hdrh_bar_name
184    specified in the specification file.
185
186    :param plot: Plot to generate.
187    :param input_data: Data to process.
188    :type plot: pandas.Series
189    :type input_data: InputData
190    """
191
192    # Transform the data
193    plot_title = plot.get(u"title", u"")
194    logging.info(
195        f"    Creating the data set for the {plot.get(u'type', u'')} "
196        f"{plot_title}."
197    )
198    data = input_data.filter_tests_by_name(
199        plot, params=[u"latency", u"parent", u"tags", u"type"])
200    if data is None or len(data[0][0]) == 0:
201        logging.error(u"No data.")
202        return
203
204    # Prepare the data for the plot
205    directions = [u"W-E", u"E-W"]
206    tests = list()
207    traces = list()
208    for idx_row, test in enumerate(data[0][0]):
209        try:
210            if test[u"type"] in (u"NDRPDR",):
211                if u"-pdr" in plot_title.lower():
212                    ttype = u"PDR"
213                elif u"-ndr" in plot_title.lower():
214                    ttype = u"NDR"
215                else:
216                    logging.warning(f"Invalid test type: {test[u'type']}")
217                    continue
218                name = re.sub(REGEX_NIC, u"", test[u"parent"].
219                              replace(u'-ndrpdr', u'').
220                              replace(u'2n1l-', u''))
221                histograms = list()
222                for idx_col, direction in enumerate(
223                        (u"direction1", u"direction2", )):
224                    try:
225                        hdr_lat = test[u"latency"][ttype][direction][u"hdrh"]
226                        # TODO: Workaround, HDRH data must be aligned to 4
227                        #       bytes, remove when not needed.
228                        hdr_lat += u"=" * (len(hdr_lat) % 4)
229                        xaxis = list()
230                        yaxis = list()
231                        hovertext = list()
232                        decoded = hdrh.histogram.HdrHistogram.decode(hdr_lat)
233                        total_count = decoded.get_total_count()
234                        for item in decoded.get_recorded_iterator():
235                            xaxis.append(item.value_iterated_to)
236                            prob = float(item.count_added_in_this_iter_step) / \
237                                   total_count * 100
238                            yaxis.append(prob)
239                            hovertext.append(
240                                f"Test: {name}<br>"
241                                f"Direction: {directions[idx_col]}<br>"
242                                f"Latency: {item.value_iterated_to}uSec<br>"
243                                f"Probability: {prob:.2f}%<br>"
244                                f"Percentile: "
245                                f"{item.percentile_level_iterated_to:.2f}"
246                            )
247                        marker_color = [COLORS[idx_row], ] * len(yaxis)
248                        marker_color[xaxis.index(
249                            decoded.get_value_at_percentile(50.0))] = u"red"
250                        marker_color[xaxis.index(
251                            decoded.get_value_at_percentile(90.0))] = u"red"
252                        marker_color[xaxis.index(
253                            decoded.get_value_at_percentile(95.0))] = u"red"
254                        histograms.append(
255                            plgo.Bar(
256                                x=xaxis,
257                                y=yaxis,
258                                showlegend=False,
259                                name=name,
260                                marker={u"color": marker_color},
261                                hovertext=hovertext,
262                                hoverinfo=u"text"
263                            )
264                        )
265                    except hdrh.codec.HdrLengthException as err:
266                        logging.warning(
267                            f"No or invalid data for HDRHistogram for the test "
268                            f"{name}\n{err}"
269                        )
270                        continue
271                if len(histograms) == 2:
272                    traces.append(histograms)
273                    tests.append(name)
274            else:
275                logging.warning(f"Invalid test type: {test[u'type']}")
276                continue
277        except (ValueError, KeyError) as err:
278            logging.warning(repr(err))
279
280    if not tests:
281        logging.warning(f"No data for {plot_title}.")
282        return
283
284    fig = make_subplots(
285        rows=len(tests),
286        cols=2,
287        specs=[
288            [{u"type": u"bar"}, {u"type": u"bar"}] for _ in range(len(tests))
289        ]
290    )
291
292    layout_axes = dict(
293        gridcolor=u"rgb(220, 220, 220)",
294        linecolor=u"rgb(220, 220, 220)",
295        linewidth=1,
296        showgrid=True,
297        showline=True,
298        showticklabels=True,
299        tickcolor=u"rgb(220, 220, 220)",
300    )
301
302    for idx_row, test in enumerate(tests):
303        for idx_col in range(2):
304            fig.add_trace(
305                traces[idx_row][idx_col],
306                row=idx_row + 1,
307                col=idx_col + 1
308            )
309            fig.update_xaxes(
310                row=idx_row + 1,
311                col=idx_col + 1,
312                **layout_axes
313            )
314            fig.update_yaxes(
315                row=idx_row + 1,
316                col=idx_col + 1,
317                **layout_axes
318            )
319
320    layout = deepcopy(plot[u"layout"])
321
322    layout[u"title"][u"text"] = \
323        f"<b>Latency:</b> {plot.get(u'graph-title', u'')}"
324    layout[u"height"] = 250 * len(tests) + 130
325
326    layout[u"annotations"][2][u"y"] = 1.06 - 0.008 * len(tests)
327    layout[u"annotations"][3][u"y"] = 1.06 - 0.008 * len(tests)
328
329    for idx, test in enumerate(tests):
330        layout[u"annotations"].append({
331            u"font": {
332                u"size": 14
333            },
334            u"showarrow": False,
335            u"text": f"<b>{test}</b>",
336            u"textangle": 0,
337            u"x": 0.5,
338            u"xanchor": u"center",
339            u"xref": u"paper",
340            u"y": 1.0 - float(idx) * 1.06 / len(tests),
341            u"yanchor": u"bottom",
342            u"yref": u"paper"
343        })
344
345    fig[u"layout"].update(layout)
346
347    # Create plot
348    file_type = plot.get(u"output-file-type", u".html")
349    logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
350    try:
351        # Export Plot
352        ploff.plot(fig, show_link=False, auto_open=False,
353                   filename=f"{plot[u'output-file']}{file_type}")
354    except PlotlyError as err:
355        logging.error(f"   Finished with error: {repr(err)}")
356
357
358def plot_nf_reconf_box_name(plot, input_data):
359    """Generate the plot(s) with algorithm: plot_nf_reconf_box_name
360    specified in the specification file.
361
362    :param plot: Plot to generate.
363    :param input_data: Data to process.
364    :type plot: pandas.Series
365    :type input_data: InputData
366    """
367
368    # Transform the data
369    logging.info(
370        f"    Creating the data set for the {plot.get(u'type', u'')} "
371        f"{plot.get(u'title', u'')}."
372    )
373    data = input_data.filter_tests_by_name(
374        plot, params=[u"result", u"parent", u"tags", u"type"]
375    )
376    if data is None:
377        logging.error(u"No data.")
378        return
379
380    # Prepare the data for the plot
381    y_vals = OrderedDict()
382    loss = dict()
383    for job in data:
384        for build in job:
385            for test in build:
386                if y_vals.get(test[u"parent"], None) is None:
387                    y_vals[test[u"parent"]] = list()
388                    loss[test[u"parent"]] = list()
389                try:
390                    y_vals[test[u"parent"]].append(test[u"result"][u"time"])
391                    loss[test[u"parent"]].append(test[u"result"][u"loss"])
392                except (KeyError, TypeError):
393                    y_vals[test[u"parent"]].append(None)
394
395    # Add None to the lists with missing data
396    max_len = 0
397    nr_of_samples = list()
398    for val in y_vals.values():
399        if len(val) > max_len:
400            max_len = len(val)
401        nr_of_samples.append(len(val))
402    for val in y_vals.values():
403        if len(val) < max_len:
404            val.extend([None for _ in range(max_len - len(val))])
405
406    # Add plot traces
407    traces = list()
408    df_y = pd.DataFrame(y_vals)
409    df_y.head()
410    for i, col in enumerate(df_y.columns):
411        tst_name = re.sub(REGEX_NIC, u"",
412                          col.lower().replace(u'-ndrpdr', u'').
413                          replace(u'2n1l-', u''))
414
415        traces.append(plgo.Box(
416            x=[str(i + 1) + u'.'] * len(df_y[col]),
417            y=[y if y else None for y in df_y[col]],
418            name=(
419                f"{i + 1}. "
420                f"({nr_of_samples[i]:02d} "
421                f"run{u's' if nr_of_samples[i] > 1 else u''}, "
422                f"packets lost average: {mean(loss[col]):.1f}) "
423                f"{u'-'.join(tst_name.split(u'-')[3:-2])}"
424            ),
425            hoverinfo=u"y+name"
426        ))
427    try:
428        # Create plot
429        layout = deepcopy(plot[u"layout"])
430        layout[u"title"] = f"<b>Time Lost:</b> {layout[u'title']}"
431        layout[u"yaxis"][u"title"] = u"<b>Implied Time Lost [s]</b>"
432        layout[u"legend"][u"font"][u"size"] = 14
433        layout[u"yaxis"].pop(u"range")
434        plpl = plgo.Figure(data=traces, layout=layout)
435
436        # Export Plot
437        file_type = plot.get(u"output-file-type", u".html")
438        logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
439        ploff.plot(
440            plpl,
441            show_link=False,
442            auto_open=False,
443            filename=f"{plot[u'output-file']}{file_type}"
444        )
445    except PlotlyError as err:
446        logging.error(
447            f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
448        )
449        return
450
451
452def plot_perf_box_name(plot, input_data):
453    """Generate the plot(s) with algorithm: plot_perf_box_name
454    specified in the specification file.
455
456    :param plot: Plot to generate.
457    :param input_data: Data to process.
458    :type plot: pandas.Series
459    :type input_data: InputData
460    """
461
462    # Transform the data
463    logging.info(
464        f"    Creating data set for the {plot.get(u'type', u'')} "
465        f"{plot.get(u'title', u'')}."
466    )
467    data = input_data.filter_tests_by_name(
468        plot, params=[u"throughput", u"parent", u"tags", u"type"])
469    if data is None:
470        logging.error(u"No data.")
471        return
472
473    # Prepare the data for the plot
474    y_vals = OrderedDict()
475    for job in data:
476        for build in job:
477            for test in build:
478                if y_vals.get(test[u"parent"], None) is None:
479                    y_vals[test[u"parent"]] = list()
480                try:
481                    if (test[u"type"] in (u"NDRPDR", ) and
482                            u"-pdr" in plot.get(u"title", u"").lower()):
483                        y_vals[test[u"parent"]].\
484                            append(test[u"throughput"][u"PDR"][u"LOWER"])
485                    elif (test[u"type"] in (u"NDRPDR", ) and
486                          u"-ndr" in plot.get(u"title", u"").lower()):
487                        y_vals[test[u"parent"]]. \
488                            append(test[u"throughput"][u"NDR"][u"LOWER"])
489                    elif test[u"type"] in (u"SOAK", ):
490                        y_vals[test[u"parent"]].\
491                            append(test[u"throughput"][u"LOWER"])
492                    else:
493                        continue
494                except (KeyError, TypeError):
495                    y_vals[test[u"parent"]].append(None)
496
497    # Add None to the lists with missing data
498    max_len = 0
499    nr_of_samples = list()
500    for val in y_vals.values():
501        if len(val) > max_len:
502            max_len = len(val)
503        nr_of_samples.append(len(val))
504    for val in y_vals.values():
505        if len(val) < max_len:
506            val.extend([None for _ in range(max_len - len(val))])
507
508    # Add plot traces
509    traces = list()
510    df_y = pd.DataFrame(y_vals)
511    df_y.head()
512    y_max = list()
513    for i, col in enumerate(df_y.columns):
514        tst_name = re.sub(REGEX_NIC, u"",
515                          col.lower().replace(u'-ndrpdr', u'').
516                          replace(u'2n1l-', u''))
517        traces.append(
518            plgo.Box(
519                x=[str(i + 1) + u'.'] * len(df_y[col]),
520                y=[y / 1000000 if y else None for y in df_y[col]],
521                name=(
522                    f"{i + 1}. "
523                    f"({nr_of_samples[i]:02d} "
524                    f"run{u's' if nr_of_samples[i] > 1 else u''}) "
525                    f"{tst_name}"
526                ),
527                hoverinfo=u"y+name"
528            )
529        )
530        try:
531            val_max = max(df_y[col])
532            if val_max:
533                y_max.append(int(val_max / 1000000) + 2)
534        except (ValueError, TypeError) as err:
535            logging.error(repr(err))
536            continue
537
538    try:
539        # Create plot
540        layout = deepcopy(plot[u"layout"])
541        if layout.get(u"title", None):
542            layout[u"title"] = f"<b>Throughput:</b> {layout[u'title']}"
543        if y_max:
544            layout[u"yaxis"][u"range"] = [0, max(y_max)]
545        plpl = plgo.Figure(data=traces, layout=layout)
546
547        # Export Plot
548        logging.info(f"    Writing file {plot[u'output-file']}.html.")
549        ploff.plot(
550            plpl,
551            show_link=False,
552            auto_open=False,
553            filename=f"{plot[u'output-file']}.html"
554        )
555    except PlotlyError as err:
556        logging.error(
557            f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
558        )
559        return
560
561
562def plot_lat_err_bars_name(plot, input_data):
563    """Generate the plot(s) with algorithm: plot_lat_err_bars_name
564    specified in the specification file.
565
566    :param plot: Plot to generate.
567    :param input_data: Data to process.
568    :type plot: pandas.Series
569    :type input_data: InputData
570    """
571
572    # Transform the data
573    plot_title = plot.get(u"title", u"")
574    logging.info(
575        f"    Creating data set for the {plot.get(u'type', u'')} {plot_title}."
576    )
577    data = input_data.filter_tests_by_name(
578        plot, params=[u"latency", u"parent", u"tags", u"type"])
579    if data is None:
580        logging.error(u"No data.")
581        return
582
583    # Prepare the data for the plot
584    y_tmp_vals = OrderedDict()
585    for job in data:
586        for build in job:
587            for test in build:
588                try:
589                    logging.debug(f"test[u'latency']: {test[u'latency']}\n")
590                except ValueError as err:
591                    logging.warning(repr(err))
592                if y_tmp_vals.get(test[u"parent"], None) is None:
593                    y_tmp_vals[test[u"parent"]] = [
594                        list(),  # direction1, min
595                        list(),  # direction1, avg
596                        list(),  # direction1, max
597                        list(),  # direction2, min
598                        list(),  # direction2, avg
599                        list()   # direction2, max
600                    ]
601                try:
602                    if test[u"type"] not in (u"NDRPDR", ):
603                        logging.warning(f"Invalid test type: {test[u'type']}")
604                        continue
605                    if u"-pdr" in plot_title.lower():
606                        ttype = u"PDR"
607                    elif u"-ndr" in plot_title.lower():
608                        ttype = u"NDR"
609                    else:
610                        logging.warning(
611                            f"Invalid test type: {test[u'type']}"
612                        )
613                        continue
614                    y_tmp_vals[test[u"parent"]][0].append(
615                        test[u"latency"][ttype][u"direction1"][u"min"])
616                    y_tmp_vals[test[u"parent"]][1].append(
617                        test[u"latency"][ttype][u"direction1"][u"avg"])
618                    y_tmp_vals[test[u"parent"]][2].append(
619                        test[u"latency"][ttype][u"direction1"][u"max"])
620                    y_tmp_vals[test[u"parent"]][3].append(
621                        test[u"latency"][ttype][u"direction2"][u"min"])
622                    y_tmp_vals[test[u"parent"]][4].append(
623                        test[u"latency"][ttype][u"direction2"][u"avg"])
624                    y_tmp_vals[test[u"parent"]][5].append(
625                        test[u"latency"][ttype][u"direction2"][u"max"])
626                except (KeyError, TypeError) as err:
627                    logging.warning(repr(err))
628
629    x_vals = list()
630    y_vals = list()
631    y_mins = list()
632    y_maxs = list()
633    nr_of_samples = list()
634    for key, val in y_tmp_vals.items():
635        name = re.sub(REGEX_NIC, u"", key.replace(u'-ndrpdr', u'').
636                      replace(u'2n1l-', u''))
637        x_vals.append(name)  # dir 1
638        y_vals.append(mean(val[1]) if val[1] else None)
639        y_mins.append(mean(val[0]) if val[0] else None)
640        y_maxs.append(mean(val[2]) if val[2] else None)
641        nr_of_samples.append(len(val[1]) if val[1] else 0)
642        x_vals.append(name)  # dir 2
643        y_vals.append(mean(val[4]) if val[4] else None)
644        y_mins.append(mean(val[3]) if val[3] else None)
645        y_maxs.append(mean(val[5]) if val[5] else None)
646        nr_of_samples.append(len(val[3]) if val[3] else 0)
647
648    traces = list()
649    annotations = list()
650
651    for idx, _ in enumerate(x_vals):
652        if not bool(int(idx % 2)):
653            direction = u"West-East"
654        else:
655            direction = u"East-West"
656        hovertext = (
657            f"No. of Runs: {nr_of_samples[idx]}<br>"
658            f"Test: {x_vals[idx]}<br>"
659            f"Direction: {direction}<br>"
660        )
661        if isinstance(y_maxs[idx], float):
662            hovertext += f"Max: {y_maxs[idx]:.2f}uSec<br>"
663        if isinstance(y_vals[idx], float):
664            hovertext += f"Mean: {y_vals[idx]:.2f}uSec<br>"
665        if isinstance(y_mins[idx], float):
666            hovertext += f"Min: {y_mins[idx]:.2f}uSec"
667
668        if isinstance(y_maxs[idx], float) and isinstance(y_vals[idx], float):
669            array = [y_maxs[idx] - y_vals[idx], ]
670        else:
671            array = [None, ]
672        if isinstance(y_mins[idx], float) and isinstance(y_vals[idx], float):
673            arrayminus = [y_vals[idx] - y_mins[idx], ]
674        else:
675            arrayminus = [None, ]
676        traces.append(plgo.Scatter(
677            x=[idx, ],
678            y=[y_vals[idx], ],
679            name=x_vals[idx],
680            legendgroup=x_vals[idx],
681            showlegend=bool(int(idx % 2)),
682            mode=u"markers",
683            error_y=dict(
684                type=u"data",
685                symmetric=False,
686                array=array,
687                arrayminus=arrayminus,
688                color=COLORS[int(idx / 2)]
689            ),
690            marker=dict(
691                size=10,
692                color=COLORS[int(idx / 2)],
693            ),
694            text=hovertext,
695            hoverinfo=u"text",
696        ))
697        annotations.append(dict(
698            x=idx,
699            y=0,
700            xref=u"x",
701            yref=u"y",
702            xanchor=u"center",
703            yanchor=u"top",
704            text=u"E-W" if bool(int(idx % 2)) else u"W-E",
705            font=dict(
706                size=16,
707            ),
708            align=u"center",
709            showarrow=False
710        ))
711
712    try:
713        # Create plot
714        file_type = plot.get(u"output-file-type", u".html")
715        logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
716        layout = deepcopy(plot[u"layout"])
717        if layout.get(u"title", None):
718            layout[u"title"] = f"<b>Latency:</b> {layout[u'title']}"
719        layout[u"annotations"] = annotations
720        plpl = plgo.Figure(data=traces, layout=layout)
721
722        # Export Plot
723        ploff.plot(
724            plpl,
725            show_link=False, auto_open=False,
726            filename=f"{plot[u'output-file']}{file_type}"
727        )
728    except PlotlyError as err:
729        logging.error(
730            f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
731        )
732        return
733
734
735def plot_tsa_name(plot, input_data):
736    """Generate the plot(s) with algorithm:
737    plot_tsa_name
738    specified in the specification file.
739
740    :param plot: Plot to generate.
741    :param input_data: Data to process.
742    :type plot: pandas.Series
743    :type input_data: InputData
744    """
745
746    # Transform the data
747    plot_title = plot.get(u"title", u"")
748    logging.info(
749        f"    Creating data set for the {plot.get(u'type', u'')} {plot_title}."
750    )
751    data = input_data.filter_tests_by_name(
752        plot, params=[u"throughput", u"parent", u"tags", u"type"])
753    if data is None:
754        logging.error(u"No data.")
755        return
756
757    y_vals = OrderedDict()
758    for job in data:
759        for build in job:
760            for test in build:
761                if y_vals.get(test[u"parent"], None) is None:
762                    y_vals[test[u"parent"]] = {
763                        u"1": list(),
764                        u"2": list(),
765                        u"4": list()
766                    }
767                try:
768                    if test[u"type"] not in (u"NDRPDR",):
769                        continue
770
771                    if u"-pdr" in plot_title.lower():
772                        ttype = u"PDR"
773                    elif u"-ndr" in plot_title.lower():
774                        ttype = u"NDR"
775                    else:
776                        continue
777
778                    if u"1C" in test[u"tags"]:
779                        y_vals[test[u"parent"]][u"1"]. \
780                            append(test[u"throughput"][ttype][u"LOWER"])
781                    elif u"2C" in test[u"tags"]:
782                        y_vals[test[u"parent"]][u"2"]. \
783                            append(test[u"throughput"][ttype][u"LOWER"])
784                    elif u"4C" in test[u"tags"]:
785                        y_vals[test[u"parent"]][u"4"]. \
786                            append(test[u"throughput"][ttype][u"LOWER"])
787                except (KeyError, TypeError):
788                    pass
789
790    if not y_vals:
791        logging.warning(f"No data for the plot {plot.get(u'title', u'')}")
792        return
793
794    y_1c_max = dict()
795    for test_name, test_vals in y_vals.items():
796        for key, test_val in test_vals.items():
797            if test_val:
798                avg_val = sum(test_val) / len(test_val)
799                y_vals[test_name][key] = [avg_val, len(test_val)]
800                ideal = avg_val / (int(key) * 1000000.0)
801                if test_name not in y_1c_max or ideal > y_1c_max[test_name]:
802                    y_1c_max[test_name] = ideal
803
804    vals = OrderedDict()
805    y_max = list()
806    nic_limit = 0
807    lnk_limit = 0
808    pci_limit = plot[u"limits"][u"pci"][u"pci-g3-x8"]
809    for test_name, test_vals in y_vals.items():
810        try:
811            if test_vals[u"1"][1]:
812                name = re.sub(
813                    REGEX_NIC,
814                    u"",
815                    test_name.replace(u'-ndrpdr', u'').replace(u'2n1l-', u'')
816                )
817                vals[name] = OrderedDict()
818                y_val_1 = test_vals[u"1"][0] / 1000000.0
819                y_val_2 = test_vals[u"2"][0] / 1000000.0 if test_vals[u"2"][0] \
820                    else None
821                y_val_4 = test_vals[u"4"][0] / 1000000.0 if test_vals[u"4"][0] \
822                    else None
823
824                vals[name][u"val"] = [y_val_1, y_val_2, y_val_4]
825                vals[name][u"rel"] = [1.0, None, None]
826                vals[name][u"ideal"] = [
827                    y_1c_max[test_name],
828                    y_1c_max[test_name] * 2,
829                    y_1c_max[test_name] * 4
830                ]
831                vals[name][u"diff"] = [
832                    (y_val_1 - y_1c_max[test_name]) * 100 / y_val_1, None, None
833                ]
834                vals[name][u"count"] = [
835                    test_vals[u"1"][1],
836                    test_vals[u"2"][1],
837                    test_vals[u"4"][1]
838                ]
839
840                try:
841                    val_max = max(vals[name][u"val"])
842                except ValueError as err:
843                    logging.error(repr(err))
844                    continue
845                if val_max:
846                    y_max.append(val_max)
847
848                if y_val_2:
849                    vals[name][u"rel"][1] = round(y_val_2 / y_val_1, 2)
850                    vals[name][u"diff"][1] = \
851                        (y_val_2 - vals[name][u"ideal"][1]) * 100 / y_val_2
852                if y_val_4:
853                    vals[name][u"rel"][2] = round(y_val_4 / y_val_1, 2)
854                    vals[name][u"diff"][2] = \
855                        (y_val_4 - vals[name][u"ideal"][2]) * 100 / y_val_4
856        except IndexError as err:
857            logging.warning(f"No data for {test_name}")
858            logging.warning(repr(err))
859
860        # Limits:
861        if u"x520" in test_name:
862            limit = plot[u"limits"][u"nic"][u"x520"]
863        elif u"x710" in test_name:
864            limit = plot[u"limits"][u"nic"][u"x710"]
865        elif u"xxv710" in test_name:
866            limit = plot[u"limits"][u"nic"][u"xxv710"]
867        elif u"xl710" in test_name:
868            limit = plot[u"limits"][u"nic"][u"xl710"]
869        elif u"x553" in test_name:
870            limit = plot[u"limits"][u"nic"][u"x553"]
871        else:
872            limit = 0
873        if limit > nic_limit:
874            nic_limit = limit
875
876        mul = 2 if u"ge2p" in test_name else 1
877        if u"10ge" in test_name:
878            limit = plot[u"limits"][u"link"][u"10ge"] * mul
879        elif u"25ge" in test_name:
880            limit = plot[u"limits"][u"link"][u"25ge"] * mul
881        elif u"40ge" in test_name:
882            limit = plot[u"limits"][u"link"][u"40ge"] * mul
883        elif u"100ge" in test_name:
884            limit = plot[u"limits"][u"link"][u"100ge"] * mul
885        else:
886            limit = 0
887        if limit > lnk_limit:
888            lnk_limit = limit
889
890    traces = list()
891    annotations = list()
892    x_vals = [1, 2, 4]
893
894    # Limits:
895    try:
896        threshold = 1.1 * max(y_max)  # 10%
897    except ValueError as err:
898        logging.error(err)
899        return
900    nic_limit /= 1000000.0
901    traces.append(plgo.Scatter(
902        x=x_vals,
903        y=[nic_limit, ] * len(x_vals),
904        name=f"NIC: {nic_limit:.2f}Mpps",
905        showlegend=False,
906        mode=u"lines",
907        line=dict(
908            dash=u"dot",
909            color=COLORS[-1],
910            width=1),
911        hoverinfo=u"none"
912    ))
913    annotations.append(dict(
914        x=1,
915        y=nic_limit,
916        xref=u"x",
917        yref=u"y",
918        xanchor=u"left",
919        yanchor=u"bottom",
920        text=f"NIC: {nic_limit:.2f}Mpps",
921        font=dict(
922            size=14,
923            color=COLORS[-1],
924        ),
925        align=u"left",
926        showarrow=False
927    ))
928    y_max.append(nic_limit)
929
930    lnk_limit /= 1000000.0
931    if lnk_limit < threshold:
932        traces.append(plgo.Scatter(
933            x=x_vals,
934            y=[lnk_limit, ] * len(x_vals),
935            name=f"Link: {lnk_limit:.2f}Mpps",
936            showlegend=False,
937            mode=u"lines",
938            line=dict(
939                dash=u"dot",
940                color=COLORS[-2],
941                width=1),
942            hoverinfo=u"none"
943        ))
944        annotations.append(dict(
945            x=1,
946            y=lnk_limit,
947            xref=u"x",
948            yref=u"y",
949            xanchor=u"left",
950            yanchor=u"bottom",
951            text=f"Link: {lnk_limit:.2f}Mpps",
952            font=dict(
953                size=14,
954                color=COLORS[-2],
955            ),
956            align=u"left",
957            showarrow=False
958        ))
959        y_max.append(lnk_limit)
960
961    pci_limit /= 1000000.0
962    if (pci_limit < threshold and
963            (pci_limit < lnk_limit * 0.95 or lnk_limit > lnk_limit * 1.05)):
964        traces.append(plgo.Scatter(
965            x=x_vals,
966            y=[pci_limit, ] * len(x_vals),
967            name=f"PCIe: {pci_limit:.2f}Mpps",
968            showlegend=False,
969            mode=u"lines",
970            line=dict(
971                dash=u"dot",
972                color=COLORS[-3],
973                width=1),
974            hoverinfo=u"none"
975        ))
976        annotations.append(dict(
977            x=1,
978            y=pci_limit,
979            xref=u"x",
980            yref=u"y",
981            xanchor=u"left",
982            yanchor=u"bottom",
983            text=f"PCIe: {pci_limit:.2f}Mpps",
984            font=dict(
985                size=14,
986                color=COLORS[-3],
987            ),
988            align=u"left",
989            showarrow=False
990        ))
991        y_max.append(pci_limit)
992
993    # Perfect and measured:
994    cidx = 0
995    for name, val in vals.items():
996        hovertext = list()
997        try:
998            for idx in range(len(val[u"val"])):
999                htext = ""
1000                if isinstance(val[u"val"][idx], float):
1001                    htext += (
1002                        f"No. of Runs: {val[u'count'][idx]}<br>"
1003                        f"Mean: {val[u'val'][idx]:.2f}Mpps<br>"
1004                    )
1005                if isinstance(val[u"diff"][idx], float):
1006                    htext += f"Diff: {round(val[u'diff'][idx]):.0f}%<br>"
1007                if isinstance(val[u"rel"][idx], float):
1008                    htext += f"Speedup: {val[u'rel'][idx]:.2f}"
1009                hovertext.append(htext)
1010            traces.append(
1011                plgo.Scatter(
1012                    x=x_vals,
1013                    y=val[u"val"],
1014                    name=name,
1015                    legendgroup=name,
1016                    mode=u"lines+markers",
1017                    line=dict(
1018                        color=COLORS[cidx],
1019                        width=2),
1020                    marker=dict(
1021                        symbol=u"circle",
1022                        size=10
1023                    ),
1024                    text=hovertext,
1025                    hoverinfo=u"text+name"
1026                )
1027            )
1028            traces.append(
1029                plgo.Scatter(
1030                    x=x_vals,
1031                    y=val[u"ideal"],
1032                    name=f"{name} perfect",
1033                    legendgroup=name,
1034                    showlegend=False,
1035                    mode=u"lines",
1036                    line=dict(
1037                        color=COLORS[cidx],
1038                        width=2,
1039                        dash=u"dash"),
1040                    text=[f"Perfect: {y:.2f}Mpps" for y in val[u"ideal"]],
1041                    hoverinfo=u"text"
1042                )
1043            )
1044            cidx += 1
1045        except (IndexError, ValueError, KeyError) as err:
1046            logging.warning(f"No data for {name}\n{repr(err)}")
1047
1048    try:
1049        # Create plot
1050        file_type = plot.get(u"output-file-type", u".html")
1051        logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
1052        layout = deepcopy(plot[u"layout"])
1053        if layout.get(u"title", None):
1054            layout[u"title"] = f"<b>Speedup Multi-core:</b> {layout[u'title']}"
1055        layout[u"yaxis"][u"range"] = [0, int(max(y_max) * 1.1)]
1056        layout[u"annotations"].extend(annotations)
1057        plpl = plgo.Figure(data=traces, layout=layout)
1058
1059        # Export Plot
1060        ploff.plot(
1061            plpl,
1062            show_link=False,
1063            auto_open=False,
1064            filename=f"{plot[u'output-file']}{file_type}"
1065        )
1066    except PlotlyError as err:
1067        logging.error(
1068            f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1069        )
1070        return
1071
1072
1073def plot_http_server_perf_box(plot, input_data):
1074    """Generate the plot(s) with algorithm: plot_http_server_perf_box
1075    specified in the specification file.
1076
1077    :param plot: Plot to generate.
1078    :param input_data: Data to process.
1079    :type plot: pandas.Series
1080    :type input_data: InputData
1081    """
1082
1083    # Transform the data
1084    logging.info(
1085        f"    Creating the data set for the {plot.get(u'type', u'')} "
1086        f"{plot.get(u'title', u'')}."
1087    )
1088    data = input_data.filter_data(plot)
1089    if data is None:
1090        logging.error(u"No data.")
1091        return
1092
1093    # Prepare the data for the plot
1094    y_vals = dict()
1095    for job in data:
1096        for build in job:
1097            for test in build:
1098                if y_vals.get(test[u"name"], None) is None:
1099                    y_vals[test[u"name"]] = list()
1100                try:
1101                    y_vals[test[u"name"]].append(test[u"result"])
1102                except (KeyError, TypeError):
1103                    y_vals[test[u"name"]].append(None)
1104
1105    # Add None to the lists with missing data
1106    max_len = 0
1107    nr_of_samples = list()
1108    for val in y_vals.values():
1109        if len(val) > max_len:
1110            max_len = len(val)
1111        nr_of_samples.append(len(val))
1112    for val in y_vals.values():
1113        if len(val) < max_len:
1114            val.extend([None for _ in range(max_len - len(val))])
1115
1116    # Add plot traces
1117    traces = list()
1118    df_y = pd.DataFrame(y_vals)
1119    df_y.head()
1120    for i, col in enumerate(df_y.columns):
1121        name = \
1122            f"{i + 1}. " \
1123            f"({nr_of_samples[i]:02d} " \
1124            f"run{u's' if nr_of_samples[i] > 1 else u''}) " \
1125            f"{col.lower().replace(u'-ndrpdr', u'')}"
1126        if len(name) > 50:
1127            name_lst = name.split(u'-')
1128            name = u""
1129            split_name = True
1130            for segment in name_lst:
1131                if (len(name) + len(segment) + 1) > 50 and split_name:
1132                    name += u"<br>    "
1133                    split_name = False
1134                name += segment + u'-'
1135            name = name[:-1]
1136
1137        traces.append(plgo.Box(x=[str(i + 1) + u'.'] * len(df_y[col]),
1138                               y=df_y[col],
1139                               name=name,
1140                               **plot[u"traces"]))
1141    try:
1142        # Create plot
1143        plpl = plgo.Figure(data=traces, layout=plot[u"layout"])
1144
1145        # Export Plot
1146        logging.info(
1147            f"    Writing file {plot[u'output-file']}"
1148            f"{plot[u'output-file-type']}."
1149        )
1150        ploff.plot(
1151            plpl,
1152            show_link=False,
1153            auto_open=False,
1154            filename=f"{plot[u'output-file']}{plot[u'output-file-type']}"
1155        )
1156    except PlotlyError as err:
1157        logging.error(
1158            f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1159        )
1160        return
1161
1162
1163def plot_nf_heatmap(plot, input_data):
1164    """Generate the plot(s) with algorithm: plot_nf_heatmap
1165    specified in the specification file.
1166
1167    :param plot: Plot to generate.
1168    :param input_data: Data to process.
1169    :type plot: pandas.Series
1170    :type input_data: InputData
1171    """
1172
1173    regex_cn = re.compile(r'^(\d*)R(\d*)C$')
1174    regex_test_name = re.compile(r'^.*-(\d+ch|\d+pl)-'
1175                                 r'(\d+mif|\d+vh)-'
1176                                 r'(\d+vm\d+t|\d+dcr\d+t).*$')
1177    vals = dict()
1178
1179    # Transform the data
1180    logging.info(
1181        f"    Creating the data set for the {plot.get(u'type', u'')} "
1182        f"{plot.get(u'title', u'')}."
1183    )
1184    data = input_data.filter_data(plot, continue_on_error=True)
1185    if data is None or data.empty:
1186        logging.error(u"No data.")
1187        return
1188
1189    for job in data:
1190        for build in job:
1191            for test in build:
1192                for tag in test[u"tags"]:
1193                    groups = re.search(regex_cn, tag)
1194                    if groups:
1195                        chain = str(groups.group(1))
1196                        node = str(groups.group(2))
1197                        break
1198                else:
1199                    continue
1200                groups = re.search(regex_test_name, test[u"name"])
1201                if groups and len(groups.groups()) == 3:
1202                    hover_name = (
1203                        f"{str(groups.group(1))}-"
1204                        f"{str(groups.group(2))}-"
1205                        f"{str(groups.group(3))}"
1206                    )
1207                else:
1208                    hover_name = u""
1209                if vals.get(chain, None) is None:
1210                    vals[chain] = dict()
1211                if vals[chain].get(node, None) is None:
1212                    vals[chain][node] = dict(
1213                        name=hover_name,
1214                        vals=list(),
1215                        nr=None,
1216                        mean=None,
1217                        stdev=None
1218                    )
1219                try:
1220                    if plot[u"include-tests"] == u"MRR":
1221                        result = test[u"result"][u"receive-rate"]
1222                    elif plot[u"include-tests"] == u"PDR":
1223                        result = test[u"throughput"][u"PDR"][u"LOWER"]
1224                    elif plot[u"include-tests"] == u"NDR":
1225                        result = test[u"throughput"][u"NDR"][u"LOWER"]
1226                    else:
1227                        result = None
1228                except TypeError:
1229                    result = None
1230
1231                if result:
1232                    vals[chain][node][u"vals"].append(result)
1233
1234    if not vals:
1235        logging.error(u"No data.")
1236        return
1237
1238    txt_chains = list()
1239    txt_nodes = list()
1240    for key_c in vals:
1241        txt_chains.append(key_c)
1242        for key_n in vals[key_c].keys():
1243            txt_nodes.append(key_n)
1244            if vals[key_c][key_n][u"vals"]:
1245                vals[key_c][key_n][u"nr"] = len(vals[key_c][key_n][u"vals"])
1246                vals[key_c][key_n][u"mean"] = \
1247                    round(mean(vals[key_c][key_n][u"vals"]) / 1000000, 1)
1248                vals[key_c][key_n][u"stdev"] = \
1249                    round(stdev(vals[key_c][key_n][u"vals"]) / 1000000, 1)
1250    txt_nodes = list(set(txt_nodes))
1251
1252    def sort_by_int(value):
1253        """Makes possible to sort a list of strings which represent integers.
1254
1255        :param value: Integer as a string.
1256        :type value: str
1257        :returns: Integer representation of input parameter 'value'.
1258        :rtype: int
1259        """
1260        return int(value)
1261
1262    txt_chains = sorted(txt_chains, key=sort_by_int)
1263    txt_nodes = sorted(txt_nodes, key=sort_by_int)
1264
1265    chains = [i + 1 for i in range(len(txt_chains))]
1266    nodes = [i + 1 for i in range(len(txt_nodes))]
1267
1268    data = [list() for _ in range(len(chains))]
1269    for chain in chains:
1270        for node in nodes:
1271            try:
1272                val = vals[txt_chains[chain - 1]][txt_nodes[node - 1]][u"mean"]
1273            except (KeyError, IndexError):
1274                val = None
1275            data[chain - 1].append(val)
1276
1277    # Color scales:
1278    my_green = [[0.0, u"rgb(235, 249, 242)"],
1279                [1.0, u"rgb(45, 134, 89)"]]
1280
1281    my_blue = [[0.0, u"rgb(236, 242, 248)"],
1282               [1.0, u"rgb(57, 115, 172)"]]
1283
1284    my_grey = [[0.0, u"rgb(230, 230, 230)"],
1285               [1.0, u"rgb(102, 102, 102)"]]
1286
1287    hovertext = list()
1288    annotations = list()
1289
1290    text = (u"Test: {name}<br>"
1291            u"Runs: {nr}<br>"
1292            u"Thput: {val}<br>"
1293            u"StDev: {stdev}")
1294
1295    for chain, _ in enumerate(txt_chains):
1296        hover_line = list()
1297        for node, _ in enumerate(txt_nodes):
1298            if data[chain][node] is not None:
1299                annotations.append(
1300                    dict(
1301                        x=node+1,
1302                        y=chain+1,
1303                        xref=u"x",
1304                        yref=u"y",
1305                        xanchor=u"center",
1306                        yanchor=u"middle",
1307                        text=str(data[chain][node]),
1308                        font=dict(
1309                            size=14,
1310                        ),
1311                        align=u"center",
1312                        showarrow=False
1313                    )
1314                )
1315                hover_line.append(text.format(
1316                    name=vals[txt_chains[chain]][txt_nodes[node]][u"name"],
1317                    nr=vals[txt_chains[chain]][txt_nodes[node]][u"nr"],
1318                    val=data[chain][node],
1319                    stdev=vals[txt_chains[chain]][txt_nodes[node]][u"stdev"]))
1320        hovertext.append(hover_line)
1321
1322    traces = [
1323        plgo.Heatmap(
1324            x=nodes,
1325            y=chains,
1326            z=data,
1327            colorbar=dict(
1328                title=plot.get(u"z-axis", u""),
1329                titleside=u"right",
1330                titlefont=dict(
1331                    size=16
1332                ),
1333                tickfont=dict(
1334                    size=16,
1335                ),
1336                tickformat=u".1f",
1337                yanchor=u"bottom",
1338                y=-0.02,
1339                len=0.925,
1340            ),
1341            showscale=True,
1342            colorscale=my_green,
1343            text=hovertext,
1344            hoverinfo=u"text"
1345        )
1346    ]
1347
1348    for idx, item in enumerate(txt_nodes):
1349        # X-axis, numbers:
1350        annotations.append(
1351            dict(
1352                x=idx+1,
1353                y=0.05,
1354                xref=u"x",
1355                yref=u"y",
1356                xanchor=u"center",
1357                yanchor=u"top",
1358                text=item,
1359                font=dict(
1360                    size=16,
1361                ),
1362                align=u"center",
1363                showarrow=False
1364            )
1365        )
1366    for idx, item in enumerate(txt_chains):
1367        # Y-axis, numbers:
1368        annotations.append(
1369            dict(
1370                x=0.35,
1371                y=idx+1,
1372                xref=u"x",
1373                yref=u"y",
1374                xanchor=u"right",
1375                yanchor=u"middle",
1376                text=item,
1377                font=dict(
1378                    size=16,
1379                ),
1380                align=u"center",
1381                showarrow=False
1382            )
1383        )
1384    # X-axis, title:
1385    annotations.append(
1386        dict(
1387            x=0.55,
1388            y=-0.15,
1389            xref=u"paper",
1390            yref=u"y",
1391            xanchor=u"center",
1392            yanchor=u"bottom",
1393            text=plot.get(u"x-axis", u""),
1394            font=dict(
1395                size=16,
1396            ),
1397            align=u"center",
1398            showarrow=False
1399        )
1400    )
1401    # Y-axis, title:
1402    annotations.append(
1403        dict(
1404            x=-0.1,
1405            y=0.5,
1406            xref=u"x",
1407            yref=u"paper",
1408            xanchor=u"center",
1409            yanchor=u"middle",
1410            text=plot.get(u"y-axis", u""),
1411            font=dict(
1412                size=16,
1413            ),
1414            align=u"center",
1415            textangle=270,
1416            showarrow=False
1417        )
1418    )
1419    updatemenus = list([
1420        dict(
1421            x=1.0,
1422            y=0.0,
1423            xanchor=u"right",
1424            yanchor=u"bottom",
1425            direction=u"up",
1426            buttons=list([
1427                dict(
1428                    args=[
1429                        {
1430                            u"colorscale": [my_green, ],
1431                            u"reversescale": False
1432                        }
1433                    ],
1434                    label=u"Green",
1435                    method=u"update"
1436                ),
1437                dict(
1438                    args=[
1439                        {
1440                            u"colorscale": [my_blue, ],
1441                            u"reversescale": False
1442                        }
1443                    ],
1444                    label=u"Blue",
1445                    method=u"update"
1446                ),
1447                dict(
1448                    args=[
1449                        {
1450                            u"colorscale": [my_grey, ],
1451                            u"reversescale": False
1452                        }
1453                    ],
1454                    label=u"Grey",
1455                    method=u"update"
1456                )
1457            ])
1458        )
1459    ])
1460
1461    try:
1462        layout = deepcopy(plot[u"layout"])
1463    except KeyError as err:
1464        logging.error(f"Finished with error: No layout defined\n{repr(err)}")
1465        return
1466
1467    layout[u"annotations"] = annotations
1468    layout[u'updatemenus'] = updatemenus
1469
1470    try:
1471        # Create plot
1472        plpl = plgo.Figure(data=traces, layout=layout)
1473
1474        # Export Plot
1475        logging.info(
1476            f"    Writing file {plot[u'output-file']}"
1477            f"{plot[u'output-file-type']}."
1478        )
1479        ploff.plot(
1480            plpl,
1481            show_link=False,
1482            auto_open=False,
1483            filename=f"{plot[u'output-file']}{plot[u'output-file-type']}"
1484        )
1485    except PlotlyError as err:
1486        logging.error(
1487            f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1488        )
1489        return
1490