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