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