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