Report: Detailed test results
[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 file_handler:
318                         file_handler.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         elif u"cx556a" in test_name:
1029             limit = plot[u"limits"][u"nic"][u"cx556a"]
1030         else:
1031             limit = 0
1032         if limit > nic_limit:
1033             nic_limit = limit
1034
1035         mul = 2 if u"ge2p" in test_name else 1
1036         if u"10ge" in test_name:
1037             limit = plot[u"limits"][u"link"][u"10ge"] * mul
1038         elif u"25ge" in test_name:
1039             limit = plot[u"limits"][u"link"][u"25ge"] * mul
1040         elif u"40ge" in test_name:
1041             limit = plot[u"limits"][u"link"][u"40ge"] * mul
1042         elif u"100ge" in test_name:
1043             limit = plot[u"limits"][u"link"][u"100ge"] * mul
1044         else:
1045             limit = 0
1046         if limit > lnk_limit:
1047             lnk_limit = limit
1048
1049     traces = list()
1050     annotations = list()
1051     x_vals = [1, 2, 4]
1052
1053     # Limits:
1054     try:
1055         threshold = 1.1 * max(y_max)  # 10%
1056     except ValueError as err:
1057         logging.error(err)
1058         return
1059     nic_limit /= 1e6
1060     traces.append(plgo.Scatter(
1061         x=x_vals,
1062         y=[nic_limit, ] * len(x_vals),
1063         name=f"NIC: {nic_limit:.2f}Mpps",
1064         showlegend=False,
1065         mode=u"lines",
1066         line=dict(
1067             dash=u"dot",
1068             color=COLORS[-1],
1069             width=1),
1070         hoverinfo=u"none"
1071     ))
1072     annotations.append(dict(
1073         x=1,
1074         y=nic_limit,
1075         xref=u"x",
1076         yref=u"y",
1077         xanchor=u"left",
1078         yanchor=u"bottom",
1079         text=f"NIC: {nic_limit:.2f}Mpps",
1080         font=dict(
1081             size=14,
1082             color=COLORS[-1],
1083         ),
1084         align=u"left",
1085         showarrow=False
1086     ))
1087     y_max.append(nic_limit)
1088
1089     lnk_limit /= 1e6
1090     if lnk_limit < threshold:
1091         traces.append(plgo.Scatter(
1092             x=x_vals,
1093             y=[lnk_limit, ] * len(x_vals),
1094             name=f"Link: {lnk_limit:.2f}Mpps",
1095             showlegend=False,
1096             mode=u"lines",
1097             line=dict(
1098                 dash=u"dot",
1099                 color=COLORS[-2],
1100                 width=1),
1101             hoverinfo=u"none"
1102         ))
1103         annotations.append(dict(
1104             x=1,
1105             y=lnk_limit,
1106             xref=u"x",
1107             yref=u"y",
1108             xanchor=u"left",
1109             yanchor=u"bottom",
1110             text=f"Link: {lnk_limit:.2f}Mpps",
1111             font=dict(
1112                 size=14,
1113                 color=COLORS[-2],
1114             ),
1115             align=u"left",
1116             showarrow=False
1117         ))
1118         y_max.append(lnk_limit)
1119
1120     pci_limit /= 1e6
1121     if (pci_limit < threshold and
1122             (pci_limit < lnk_limit * 0.95 or lnk_limit > lnk_limit * 1.05)):
1123         traces.append(plgo.Scatter(
1124             x=x_vals,
1125             y=[pci_limit, ] * len(x_vals),
1126             name=f"PCIe: {pci_limit:.2f}Mpps",
1127             showlegend=False,
1128             mode=u"lines",
1129             line=dict(
1130                 dash=u"dot",
1131                 color=COLORS[-3],
1132                 width=1),
1133             hoverinfo=u"none"
1134         ))
1135         annotations.append(dict(
1136             x=1,
1137             y=pci_limit,
1138             xref=u"x",
1139             yref=u"y",
1140             xanchor=u"left",
1141             yanchor=u"bottom",
1142             text=f"PCIe: {pci_limit:.2f}Mpps",
1143             font=dict(
1144                 size=14,
1145                 color=COLORS[-3],
1146             ),
1147             align=u"left",
1148             showarrow=False
1149         ))
1150         y_max.append(pci_limit)
1151
1152     # Perfect and measured:
1153     cidx = 0
1154     for name, val in vals.items():
1155         hovertext = list()
1156         try:
1157             for idx in range(len(val[u"val"])):
1158                 htext = ""
1159                 if isinstance(val[u"val"][idx], float):
1160                     htext += (
1161                         f"No. of Runs: {val[u'count'][idx]}<br>"
1162                         f"Mean: {val[u'val'][idx]:.2f}Mpps<br>"
1163                     )
1164                 if isinstance(val[u"diff"][idx], float):
1165                     htext += f"Diff: {round(val[u'diff'][idx]):.0f}%<br>"
1166                 if isinstance(val[u"rel"][idx], float):
1167                     htext += f"Speedup: {val[u'rel'][idx]:.2f}"
1168                 hovertext.append(htext)
1169             traces.append(
1170                 plgo.Scatter(
1171                     x=x_vals,
1172                     y=val[u"val"],
1173                     name=name,
1174                     legendgroup=name,
1175                     mode=u"lines+markers",
1176                     line=dict(
1177                         color=COLORS[cidx],
1178                         width=2),
1179                     marker=dict(
1180                         symbol=u"circle",
1181                         size=10
1182                     ),
1183                     text=hovertext,
1184                     hoverinfo=u"text+name"
1185                 )
1186             )
1187             traces.append(
1188                 plgo.Scatter(
1189                     x=x_vals,
1190                     y=val[u"ideal"],
1191                     name=f"{name} perfect",
1192                     legendgroup=name,
1193                     showlegend=False,
1194                     mode=u"lines",
1195                     line=dict(
1196                         color=COLORS[cidx],
1197                         width=2,
1198                         dash=u"dash"),
1199                     text=[f"Perfect: {y:.2f}Mpps" for y in val[u"ideal"]],
1200                     hoverinfo=u"text"
1201                 )
1202             )
1203             cidx += 1
1204         except (IndexError, ValueError, KeyError) as err:
1205             logging.warning(f"No data for {name}\n{repr(err)}")
1206
1207     try:
1208         # Create plot
1209         file_type = plot.get(u"output-file-type", u".html")
1210         logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
1211         layout = deepcopy(plot[u"layout"])
1212         if layout.get(u"title", None):
1213             layout[u"title"] = f"<b>Speedup Multi-core:</b> {layout[u'title']}"
1214         layout[u"yaxis"][u"range"] = [0, int(max(y_max) * 1.1)]
1215         layout[u"annotations"].extend(annotations)
1216         plpl = plgo.Figure(data=traces, layout=layout)
1217
1218         # Export Plot
1219         ploff.plot(
1220             plpl,
1221             show_link=False,
1222             auto_open=False,
1223             filename=f"{plot[u'output-file']}{file_type}"
1224         )
1225     except PlotlyError as err:
1226         logging.error(
1227             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1228         )
1229         return
1230
1231
1232 def plot_http_server_perf_box(plot, input_data):
1233     """Generate the plot(s) with algorithm: plot_http_server_perf_box
1234     specified in the specification file.
1235
1236     :param plot: Plot to generate.
1237     :param input_data: Data to process.
1238     :type plot: pandas.Series
1239     :type input_data: InputData
1240     """
1241
1242     # Transform the data
1243     logging.info(
1244         f"    Creating the data set for the {plot.get(u'type', u'')} "
1245         f"{plot.get(u'title', u'')}."
1246     )
1247     data = input_data.filter_data(plot)
1248     if data is None:
1249         logging.error(u"No data.")
1250         return
1251
1252     # Prepare the data for the plot
1253     y_vals = dict()
1254     for job in data:
1255         for build in job:
1256             for test in build:
1257                 if y_vals.get(test[u"name"], None) is None:
1258                     y_vals[test[u"name"]] = list()
1259                 try:
1260                     y_vals[test[u"name"]].append(test[u"result"])
1261                 except (KeyError, TypeError):
1262                     y_vals[test[u"name"]].append(None)
1263
1264     # Add None to the lists with missing data
1265     max_len = 0
1266     nr_of_samples = list()
1267     for val in y_vals.values():
1268         if len(val) > max_len:
1269             max_len = len(val)
1270         nr_of_samples.append(len(val))
1271     for val in y_vals.values():
1272         if len(val) < max_len:
1273             val.extend([None for _ in range(max_len - len(val))])
1274
1275     # Add plot traces
1276     traces = list()
1277     df_y = pd.DataFrame(y_vals)
1278     df_y.head()
1279     for i, col in enumerate(df_y.columns):
1280         name = \
1281             f"{i + 1}. " \
1282             f"({nr_of_samples[i]:02d} " \
1283             f"run{u's' if nr_of_samples[i] > 1 else u''}) " \
1284             f"{col.lower().replace(u'-ndrpdr', u'')}"
1285         if len(name) > 50:
1286             name_lst = name.split(u'-')
1287             name = u""
1288             split_name = True
1289             for segment in name_lst:
1290                 if (len(name) + len(segment) + 1) > 50 and split_name:
1291                     name += u"<br>    "
1292                     split_name = False
1293                 name += segment + u'-'
1294             name = name[:-1]
1295
1296         traces.append(plgo.Box(x=[str(i + 1) + u'.'] * len(df_y[col]),
1297                                y=df_y[col],
1298                                name=name,
1299                                **plot[u"traces"]))
1300     try:
1301         # Create plot
1302         plpl = plgo.Figure(data=traces, layout=plot[u"layout"])
1303
1304         # Export Plot
1305         logging.info(
1306             f"    Writing file {plot[u'output-file']}"
1307             f"{plot[u'output-file-type']}."
1308         )
1309         ploff.plot(
1310             plpl,
1311             show_link=False,
1312             auto_open=False,
1313             filename=f"{plot[u'output-file']}{plot[u'output-file-type']}"
1314         )
1315     except PlotlyError as err:
1316         logging.error(
1317             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1318         )
1319         return
1320
1321
1322 def plot_nf_heatmap(plot, input_data):
1323     """Generate the plot(s) with algorithm: plot_nf_heatmap
1324     specified in the specification file.
1325
1326     :param plot: Plot to generate.
1327     :param input_data: Data to process.
1328     :type plot: pandas.Series
1329     :type input_data: InputData
1330     """
1331
1332     regex_cn = re.compile(r'^(\d*)R(\d*)C$')
1333     regex_test_name = re.compile(r'^.*-(\d+ch|\d+pl)-'
1334                                  r'(\d+mif|\d+vh)-'
1335                                  r'(\d+vm\d+t|\d+dcr\d+t|\d+dcr\d+c).*$')
1336     vals = dict()
1337
1338     # Transform the data
1339     logging.info(
1340         f"    Creating the data set for the {plot.get(u'type', u'')} "
1341         f"{plot.get(u'title', u'')}."
1342     )
1343     data = input_data.filter_data(plot, continue_on_error=True)
1344     if data is None or data.empty:
1345         logging.error(u"No data.")
1346         return
1347
1348     for job in data:
1349         for build in job:
1350             for test in build:
1351                 for tag in test[u"tags"]:
1352                     groups = re.search(regex_cn, tag)
1353                     if groups:
1354                         chain = str(groups.group(1))
1355                         node = str(groups.group(2))
1356                         break
1357                 else:
1358                     continue
1359                 groups = re.search(regex_test_name, test[u"name"])
1360                 if groups and len(groups.groups()) == 3:
1361                     hover_name = (
1362                         f"{str(groups.group(1))}-"
1363                         f"{str(groups.group(2))}-"
1364                         f"{str(groups.group(3))}"
1365                     )
1366                 else:
1367                     hover_name = u""
1368                 if vals.get(chain, None) is None:
1369                     vals[chain] = dict()
1370                 if vals[chain].get(node, None) is None:
1371                     vals[chain][node] = dict(
1372                         name=hover_name,
1373                         vals=list(),
1374                         nr=None,
1375                         mean=None,
1376                         stdev=None
1377                     )
1378                 try:
1379                     if plot[u"include-tests"] == u"MRR":
1380                         result = test[u"result"][u"receive-rate"]
1381                     elif plot[u"include-tests"] == u"PDR":
1382                         result = test[u"throughput"][u"PDR"][u"LOWER"]
1383                     elif plot[u"include-tests"] == u"NDR":
1384                         result = test[u"throughput"][u"NDR"][u"LOWER"]
1385                     else:
1386                         result = None
1387                 except TypeError:
1388                     result = None
1389
1390                 if result:
1391                     vals[chain][node][u"vals"].append(result)
1392
1393     if not vals:
1394         logging.error(u"No data.")
1395         return
1396
1397     txt_chains = list()
1398     txt_nodes = list()
1399     for key_c in vals:
1400         txt_chains.append(key_c)
1401         for key_n in vals[key_c].keys():
1402             txt_nodes.append(key_n)
1403             if vals[key_c][key_n][u"vals"]:
1404                 vals[key_c][key_n][u"nr"] = len(vals[key_c][key_n][u"vals"])
1405                 vals[key_c][key_n][u"mean"] = \
1406                     round(mean(vals[key_c][key_n][u"vals"]) / 1000000, 1)
1407                 vals[key_c][key_n][u"stdev"] = \
1408                     round(stdev(vals[key_c][key_n][u"vals"]) / 1000000, 1)
1409     txt_nodes = list(set(txt_nodes))
1410
1411     def sort_by_int(value):
1412         """Makes possible to sort a list of strings which represent integers.
1413
1414         :param value: Integer as a string.
1415         :type value: str
1416         :returns: Integer representation of input parameter 'value'.
1417         :rtype: int
1418         """
1419         return int(value)
1420
1421     txt_chains = sorted(txt_chains, key=sort_by_int)
1422     txt_nodes = sorted(txt_nodes, key=sort_by_int)
1423
1424     chains = [i + 1 for i in range(len(txt_chains))]
1425     nodes = [i + 1 for i in range(len(txt_nodes))]
1426
1427     data = [list() for _ in range(len(chains))]
1428     for chain in chains:
1429         for node in nodes:
1430             try:
1431                 val = vals[txt_chains[chain - 1]][txt_nodes[node - 1]][u"mean"]
1432             except (KeyError, IndexError):
1433                 val = None
1434             data[chain - 1].append(val)
1435
1436     # Color scales:
1437     my_green = [[0.0, u"rgb(235, 249, 242)"],
1438                 [1.0, u"rgb(45, 134, 89)"]]
1439
1440     my_blue = [[0.0, u"rgb(236, 242, 248)"],
1441                [1.0, u"rgb(57, 115, 172)"]]
1442
1443     my_grey = [[0.0, u"rgb(230, 230, 230)"],
1444                [1.0, u"rgb(102, 102, 102)"]]
1445
1446     hovertext = list()
1447     annotations = list()
1448
1449     text = (u"Test: {name}<br>"
1450             u"Runs: {nr}<br>"
1451             u"Thput: {val}<br>"
1452             u"StDev: {stdev}")
1453
1454     for chain, _ in enumerate(txt_chains):
1455         hover_line = list()
1456         for node, _ in enumerate(txt_nodes):
1457             if data[chain][node] is not None:
1458                 annotations.append(
1459                     dict(
1460                         x=node+1,
1461                         y=chain+1,
1462                         xref=u"x",
1463                         yref=u"y",
1464                         xanchor=u"center",
1465                         yanchor=u"middle",
1466                         text=str(data[chain][node]),
1467                         font=dict(
1468                             size=14,
1469                         ),
1470                         align=u"center",
1471                         showarrow=False
1472                     )
1473                 )
1474                 hover_line.append(text.format(
1475                     name=vals[txt_chains[chain]][txt_nodes[node]][u"name"],
1476                     nr=vals[txt_chains[chain]][txt_nodes[node]][u"nr"],
1477                     val=data[chain][node],
1478                     stdev=vals[txt_chains[chain]][txt_nodes[node]][u"stdev"]))
1479         hovertext.append(hover_line)
1480
1481     traces = [
1482         plgo.Heatmap(
1483             x=nodes,
1484             y=chains,
1485             z=data,
1486             colorbar=dict(
1487                 title=plot.get(u"z-axis", u""),
1488                 titleside=u"right",
1489                 titlefont=dict(
1490                     size=16
1491                 ),
1492                 tickfont=dict(
1493                     size=16,
1494                 ),
1495                 tickformat=u".1f",
1496                 yanchor=u"bottom",
1497                 y=-0.02,
1498                 len=0.925,
1499             ),
1500             showscale=True,
1501             colorscale=my_green,
1502             text=hovertext,
1503             hoverinfo=u"text"
1504         )
1505     ]
1506
1507     for idx, item in enumerate(txt_nodes):
1508         # X-axis, numbers:
1509         annotations.append(
1510             dict(
1511                 x=idx+1,
1512                 y=0.05,
1513                 xref=u"x",
1514                 yref=u"y",
1515                 xanchor=u"center",
1516                 yanchor=u"top",
1517                 text=item,
1518                 font=dict(
1519                     size=16,
1520                 ),
1521                 align=u"center",
1522                 showarrow=False
1523             )
1524         )
1525     for idx, item in enumerate(txt_chains):
1526         # Y-axis, numbers:
1527         annotations.append(
1528             dict(
1529                 x=0.35,
1530                 y=idx+1,
1531                 xref=u"x",
1532                 yref=u"y",
1533                 xanchor=u"right",
1534                 yanchor=u"middle",
1535                 text=item,
1536                 font=dict(
1537                     size=16,
1538                 ),
1539                 align=u"center",
1540                 showarrow=False
1541             )
1542         )
1543     # X-axis, title:
1544     annotations.append(
1545         dict(
1546             x=0.55,
1547             y=-0.15,
1548             xref=u"paper",
1549             yref=u"y",
1550             xanchor=u"center",
1551             yanchor=u"bottom",
1552             text=plot.get(u"x-axis", u""),
1553             font=dict(
1554                 size=16,
1555             ),
1556             align=u"center",
1557             showarrow=False
1558         )
1559     )
1560     # Y-axis, title:
1561     annotations.append(
1562         dict(
1563             x=-0.1,
1564             y=0.5,
1565             xref=u"x",
1566             yref=u"paper",
1567             xanchor=u"center",
1568             yanchor=u"middle",
1569             text=plot.get(u"y-axis", u""),
1570             font=dict(
1571                 size=16,
1572             ),
1573             align=u"center",
1574             textangle=270,
1575             showarrow=False
1576         )
1577     )
1578     updatemenus = list([
1579         dict(
1580             x=1.0,
1581             y=0.0,
1582             xanchor=u"right",
1583             yanchor=u"bottom",
1584             direction=u"up",
1585             buttons=list([
1586                 dict(
1587                     args=[
1588                         {
1589                             u"colorscale": [my_green, ],
1590                             u"reversescale": False
1591                         }
1592                     ],
1593                     label=u"Green",
1594                     method=u"update"
1595                 ),
1596                 dict(
1597                     args=[
1598                         {
1599                             u"colorscale": [my_blue, ],
1600                             u"reversescale": False
1601                         }
1602                     ],
1603                     label=u"Blue",
1604                     method=u"update"
1605                 ),
1606                 dict(
1607                     args=[
1608                         {
1609                             u"colorscale": [my_grey, ],
1610                             u"reversescale": False
1611                         }
1612                     ],
1613                     label=u"Grey",
1614                     method=u"update"
1615                 )
1616             ])
1617         )
1618     ])
1619
1620     try:
1621         layout = deepcopy(plot[u"layout"])
1622     except KeyError as err:
1623         logging.error(f"Finished with error: No layout defined\n{repr(err)}")
1624         return
1625
1626     layout[u"annotations"] = annotations
1627     layout[u'updatemenus'] = updatemenus
1628
1629     try:
1630         # Create plot
1631         plpl = plgo.Figure(data=traces, layout=layout)
1632
1633         # Export Plot
1634         logging.info(f"    Writing file {plot[u'output-file']}.html")
1635         ploff.plot(
1636             plpl,
1637             show_link=False,
1638             auto_open=False,
1639             filename=f"{plot[u'output-file']}.html"
1640         )
1641     except PlotlyError as err:
1642         logging.error(
1643             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1644         )
1645         return