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