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