8af9a945d12d92d06dbd6085d4361e7830368175
[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 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
182 def 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
358 def 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
452 def 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
562 def 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
735 def 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
1073 def 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
1163 def 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