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