PAL: Process Hoststack data
[csit.git] / resources / tools / presentation / generator_plots.py
1 # Copyright (c) 2020 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Algorithms to generate plots.
15 """
16
17
18 import re
19 import logging
20
21 from collections import OrderedDict
22 from copy import deepcopy
23
24 import hdrh.histogram
25 import hdrh.codec
26 import pandas as pd
27 import plotly.offline as ploff
28 import plotly.graph_objs as plgo
29
30 from plotly.subplots import make_subplots
31 from plotly.exceptions import PlotlyError
32
33 from pal_utils import mean, stdev
34
35
36 COLORS = [u"SkyBlue", u"Olive", u"Purple", u"Coral", u"Indigo", u"Pink",
37           u"Chocolate", u"Brown", u"Magenta", u"Cyan", u"Orange", u"Black",
38           u"Violet", u"Blue", u"Yellow", u"BurlyWood", u"CadetBlue", u"Crimson",
39           u"DarkBlue", u"DarkCyan", u"DarkGreen", u"Green", u"GoldenRod",
40           u"LightGreen", u"LightSeaGreen", u"LightSkyBlue", u"Maroon",
41           u"MediumSeaGreen", u"SeaGreen", u"LightSlateGrey"]
42
43 REGEX_NIC = re.compile(r'(\d*ge\dp\d\D*\d*[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"result", 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     test_type = u""
633     for job in data:
634         for build in job:
635             for test in build:
636                 if y_vals.get(test[u"parent"], None) is None:
637                     y_vals[test[u"parent"]] = list()
638                 try:
639                     if (test[u"type"] in (u"NDRPDR", ) and
640                             u"-pdr" in plot.get(u"title", u"").lower()):
641                         y_vals[test[u"parent"]].\
642                             append(test[u"throughput"][u"PDR"][u"LOWER"])
643                         test_type = u"NDRPDR"
644                     elif (test[u"type"] in (u"NDRPDR", ) and
645                           u"-ndr" in plot.get(u"title", u"").lower()):
646                         y_vals[test[u"parent"]]. \
647                             append(test[u"throughput"][u"NDR"][u"LOWER"])
648                         test_type = u"NDRPDR"
649                     elif test[u"type"] in (u"SOAK", ):
650                         y_vals[test[u"parent"]].\
651                             append(test[u"throughput"][u"LOWER"])
652                         test_type = u"SOAK"
653                     elif test[u"type"] in (u"HOSTSTACK", ):
654                         if u"LDPRELOAD" in test[u"tags"]:
655                             y_vals[test[u"parent"]].append(
656                                 float(test[u"result"][u"bits_per_second"]) / 1e3
657                             )
658                         elif u"VPPECHO" in test[u"tags"]:
659                             y_vals[test[u"parent"]].append(
660                                 (float(test[u"result"][u"client"][u"tx_data"])
661                                  * 8 / 1e3) /
662                                 ((float(test[u"result"][u"client"][u"time"]) +
663                                   float(test[u"result"][u"server"][u"time"])) /
664                                  2)
665                             )
666                         test_type = u"HOSTSTACK"
667                     else:
668                         continue
669                 except (KeyError, TypeError):
670                     y_vals[test[u"parent"]].append(None)
671
672     # Add None to the lists with missing data
673     max_len = 0
674     nr_of_samples = list()
675     for val in y_vals.values():
676         if len(val) > max_len:
677             max_len = len(val)
678         nr_of_samples.append(len(val))
679     for val in y_vals.values():
680         if len(val) < max_len:
681             val.extend([None for _ in range(max_len - len(val))])
682
683     # Add plot traces
684     traces = list()
685     df_y = pd.DataFrame(y_vals)
686     df_y.head()
687     y_max = list()
688     for i, col in enumerate(df_y.columns):
689         tst_name = re.sub(REGEX_NIC, u"",
690                           col.lower().replace(u'-ndrpdr', u'').
691                           replace(u'2n1l-', u''))
692         traces.append(
693             plgo.Box(
694                 x=[str(i + 1) + u'.'] * len(df_y[col]),
695                 y=[y / 1000000 if y else None for y in df_y[col]],
696                 name=(
697                     f"{i + 1}. "
698                     f"({nr_of_samples[i]:02d} "
699                     f"run{u's' if nr_of_samples[i] > 1 else u''}) "
700                     f"{tst_name}"
701                 ),
702                 hoverinfo=u"y+name"
703             )
704         )
705         try:
706             val_max = max(df_y[col])
707             if val_max:
708                 y_max.append(int(val_max / 1000000) + 2)
709         except (ValueError, TypeError) as err:
710             logging.error(repr(err))
711             continue
712
713     try:
714         # Create plot
715         layout = deepcopy(plot[u"layout"])
716         if layout.get(u"title", None):
717             if test_type in (u"HOSTSTACK", ):
718                 layout[u"title"] = f"<b>Bandwidth:</b> {layout[u'title']}"
719             else:
720                 layout[u"title"] = f"<b>Throughput:</b> {layout[u'title']}"
721         if y_max:
722             layout[u"yaxis"][u"range"] = [0, max(y_max)]
723         plpl = plgo.Figure(data=traces, layout=layout)
724
725         # Export Plot
726         logging.info(f"    Writing file {plot[u'output-file']}.html.")
727         ploff.plot(
728             plpl,
729             show_link=False,
730             auto_open=False,
731             filename=f"{plot[u'output-file']}.html"
732         )
733     except PlotlyError as err:
734         logging.error(
735             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
736         )
737         return
738
739
740 def plot_lat_err_bars_name(plot, input_data):
741     """Generate the plot(s) with algorithm: plot_lat_err_bars_name
742     specified in the specification file.
743
744     :param plot: Plot to generate.
745     :param input_data: Data to process.
746     :type plot: pandas.Series
747     :type input_data: InputData
748     """
749
750     # Transform the data
751     plot_title = plot.get(u"title", u"")
752     logging.info(
753         f"    Creating data set for the {plot.get(u'type', u'')} {plot_title}."
754     )
755     data = input_data.filter_tests_by_name(
756         plot, params=[u"latency", u"parent", u"tags", u"type"])
757     if data is None:
758         logging.error(u"No data.")
759         return
760
761     # Prepare the data for the plot
762     y_tmp_vals = OrderedDict()
763     for job in data:
764         for build in job:
765             for test in build:
766                 try:
767                     logging.debug(f"test[u'latency']: {test[u'latency']}\n")
768                 except ValueError as err:
769                     logging.warning(repr(err))
770                 if y_tmp_vals.get(test[u"parent"], None) is None:
771                     y_tmp_vals[test[u"parent"]] = [
772                         list(),  # direction1, min
773                         list(),  # direction1, avg
774                         list(),  # direction1, max
775                         list(),  # direction2, min
776                         list(),  # direction2, avg
777                         list()   # direction2, max
778                     ]
779                 try:
780                     if test[u"type"] not in (u"NDRPDR", ):
781                         logging.warning(f"Invalid test type: {test[u'type']}")
782                         continue
783                     if u"-pdr" in plot_title.lower():
784                         ttype = u"PDR"
785                     elif u"-ndr" in plot_title.lower():
786                         ttype = u"NDR"
787                     else:
788                         logging.warning(
789                             f"Invalid test type: {test[u'type']}"
790                         )
791                         continue
792                     y_tmp_vals[test[u"parent"]][0].append(
793                         test[u"latency"][ttype][u"direction1"][u"min"])
794                     y_tmp_vals[test[u"parent"]][1].append(
795                         test[u"latency"][ttype][u"direction1"][u"avg"])
796                     y_tmp_vals[test[u"parent"]][2].append(
797                         test[u"latency"][ttype][u"direction1"][u"max"])
798                     y_tmp_vals[test[u"parent"]][3].append(
799                         test[u"latency"][ttype][u"direction2"][u"min"])
800                     y_tmp_vals[test[u"parent"]][4].append(
801                         test[u"latency"][ttype][u"direction2"][u"avg"])
802                     y_tmp_vals[test[u"parent"]][5].append(
803                         test[u"latency"][ttype][u"direction2"][u"max"])
804                 except (KeyError, TypeError) as err:
805                     logging.warning(repr(err))
806
807     x_vals = list()
808     y_vals = list()
809     y_mins = list()
810     y_maxs = list()
811     nr_of_samples = list()
812     for key, val in y_tmp_vals.items():
813         name = re.sub(REGEX_NIC, u"", key.replace(u'-ndrpdr', u'').
814                       replace(u'2n1l-', u''))
815         x_vals.append(name)  # dir 1
816         y_vals.append(mean(val[1]) if val[1] else None)
817         y_mins.append(mean(val[0]) if val[0] else None)
818         y_maxs.append(mean(val[2]) if val[2] else None)
819         nr_of_samples.append(len(val[1]) if val[1] else 0)
820         x_vals.append(name)  # dir 2
821         y_vals.append(mean(val[4]) if val[4] else None)
822         y_mins.append(mean(val[3]) if val[3] else None)
823         y_maxs.append(mean(val[5]) if val[5] else None)
824         nr_of_samples.append(len(val[3]) if val[3] else 0)
825
826     traces = list()
827     annotations = list()
828
829     for idx, _ in enumerate(x_vals):
830         if not bool(int(idx % 2)):
831             direction = u"West-East"
832         else:
833             direction = u"East-West"
834         hovertext = (
835             f"No. of Runs: {nr_of_samples[idx]}<br>"
836             f"Test: {x_vals[idx]}<br>"
837             f"Direction: {direction}<br>"
838         )
839         if isinstance(y_maxs[idx], float):
840             hovertext += f"Max: {y_maxs[idx]:.2f}uSec<br>"
841         if isinstance(y_vals[idx], float):
842             hovertext += f"Mean: {y_vals[idx]:.2f}uSec<br>"
843         if isinstance(y_mins[idx], float):
844             hovertext += f"Min: {y_mins[idx]:.2f}uSec"
845
846         if isinstance(y_maxs[idx], float) and isinstance(y_vals[idx], float):
847             array = [y_maxs[idx] - y_vals[idx], ]
848         else:
849             array = [None, ]
850         if isinstance(y_mins[idx], float) and isinstance(y_vals[idx], float):
851             arrayminus = [y_vals[idx] - y_mins[idx], ]
852         else:
853             arrayminus = [None, ]
854         traces.append(plgo.Scatter(
855             x=[idx, ],
856             y=[y_vals[idx], ],
857             name=x_vals[idx],
858             legendgroup=x_vals[idx],
859             showlegend=bool(int(idx % 2)),
860             mode=u"markers",
861             error_y=dict(
862                 type=u"data",
863                 symmetric=False,
864                 array=array,
865                 arrayminus=arrayminus,
866                 color=COLORS[int(idx / 2)]
867             ),
868             marker=dict(
869                 size=10,
870                 color=COLORS[int(idx / 2)],
871             ),
872             text=hovertext,
873             hoverinfo=u"text",
874         ))
875         annotations.append(dict(
876             x=idx,
877             y=0,
878             xref=u"x",
879             yref=u"y",
880             xanchor=u"center",
881             yanchor=u"top",
882             text=u"E-W" if bool(int(idx % 2)) else u"W-E",
883             font=dict(
884                 size=16,
885             ),
886             align=u"center",
887             showarrow=False
888         ))
889
890     try:
891         # Create plot
892         file_type = plot.get(u"output-file-type", u".html")
893         logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
894         layout = deepcopy(plot[u"layout"])
895         if layout.get(u"title", None):
896             layout[u"title"] = f"<b>Latency:</b> {layout[u'title']}"
897         layout[u"annotations"] = annotations
898         plpl = plgo.Figure(data=traces, layout=layout)
899
900         # Export Plot
901         ploff.plot(
902             plpl,
903             show_link=False, auto_open=False,
904             filename=f"{plot[u'output-file']}{file_type}"
905         )
906     except PlotlyError as err:
907         logging.error(
908             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
909         )
910         return
911
912
913 def plot_tsa_name(plot, input_data):
914     """Generate the plot(s) with algorithm:
915     plot_tsa_name
916     specified in the specification file.
917
918     :param plot: Plot to generate.
919     :param input_data: Data to process.
920     :type plot: pandas.Series
921     :type input_data: InputData
922     """
923
924     # Transform the data
925     plot_title = plot.get(u"title", u"")
926     logging.info(
927         f"    Creating data set for the {plot.get(u'type', u'')} {plot_title}."
928     )
929     data = input_data.filter_tests_by_name(
930         plot, params=[u"throughput", u"parent", u"tags", u"type"])
931     if data is None:
932         logging.error(u"No data.")
933         return
934
935     y_vals = OrderedDict()
936     for job in data:
937         for build in job:
938             for test in build:
939                 if y_vals.get(test[u"parent"], None) is None:
940                     y_vals[test[u"parent"]] = {
941                         u"1": list(),
942                         u"2": list(),
943                         u"4": list()
944                     }
945                 try:
946                     if test[u"type"] not in (u"NDRPDR",):
947                         continue
948
949                     if u"-pdr" in plot_title.lower():
950                         ttype = u"PDR"
951                     elif u"-ndr" in plot_title.lower():
952                         ttype = u"NDR"
953                     else:
954                         continue
955
956                     if u"1C" in test[u"tags"]:
957                         y_vals[test[u"parent"]][u"1"]. \
958                             append(test[u"throughput"][ttype][u"LOWER"])
959                     elif u"2C" in test[u"tags"]:
960                         y_vals[test[u"parent"]][u"2"]. \
961                             append(test[u"throughput"][ttype][u"LOWER"])
962                     elif u"4C" in test[u"tags"]:
963                         y_vals[test[u"parent"]][u"4"]. \
964                             append(test[u"throughput"][ttype][u"LOWER"])
965                 except (KeyError, TypeError):
966                     pass
967
968     if not y_vals:
969         logging.warning(f"No data for the plot {plot.get(u'title', u'')}")
970         return
971
972     y_1c_max = dict()
973     for test_name, test_vals in y_vals.items():
974         for key, test_val in test_vals.items():
975             if test_val:
976                 avg_val = sum(test_val) / len(test_val)
977                 y_vals[test_name][key] = [avg_val, len(test_val)]
978                 ideal = avg_val / (int(key) * 1000000.0)
979                 if test_name not in y_1c_max or ideal > y_1c_max[test_name]:
980                     y_1c_max[test_name] = ideal
981
982     vals = OrderedDict()
983     y_max = list()
984     nic_limit = 0
985     lnk_limit = 0
986     pci_limit = plot[u"limits"][u"pci"][u"pci-g3-x8"]
987     for test_name, test_vals in y_vals.items():
988         try:
989             if test_vals[u"1"][1]:
990                 name = re.sub(
991                     REGEX_NIC,
992                     u"",
993                     test_name.replace(u'-ndrpdr', u'').replace(u'2n1l-', u'')
994                 )
995                 vals[name] = OrderedDict()
996                 y_val_1 = test_vals[u"1"][0] / 1000000.0
997                 y_val_2 = test_vals[u"2"][0] / 1000000.0 if test_vals[u"2"][0] \
998                     else None
999                 y_val_4 = test_vals[u"4"][0] / 1000000.0 if test_vals[u"4"][0] \
1000                     else None
1001
1002                 vals[name][u"val"] = [y_val_1, y_val_2, y_val_4]
1003                 vals[name][u"rel"] = [1.0, None, None]
1004                 vals[name][u"ideal"] = [
1005                     y_1c_max[test_name],
1006                     y_1c_max[test_name] * 2,
1007                     y_1c_max[test_name] * 4
1008                 ]
1009                 vals[name][u"diff"] = [
1010                     (y_val_1 - y_1c_max[test_name]) * 100 / y_val_1, None, None
1011                 ]
1012                 vals[name][u"count"] = [
1013                     test_vals[u"1"][1],
1014                     test_vals[u"2"][1],
1015                     test_vals[u"4"][1]
1016                 ]
1017
1018                 try:
1019                     val_max = max(vals[name][u"val"])
1020                 except ValueError as err:
1021                     logging.error(repr(err))
1022                     continue
1023                 if val_max:
1024                     y_max.append(val_max)
1025
1026                 if y_val_2:
1027                     vals[name][u"rel"][1] = round(y_val_2 / y_val_1, 2)
1028                     vals[name][u"diff"][1] = \
1029                         (y_val_2 - vals[name][u"ideal"][1]) * 100 / y_val_2
1030                 if y_val_4:
1031                     vals[name][u"rel"][2] = round(y_val_4 / y_val_1, 2)
1032                     vals[name][u"diff"][2] = \
1033                         (y_val_4 - vals[name][u"ideal"][2]) * 100 / y_val_4
1034         except IndexError as err:
1035             logging.warning(f"No data for {test_name}")
1036             logging.warning(repr(err))
1037
1038         # Limits:
1039         if u"x520" in test_name:
1040             limit = plot[u"limits"][u"nic"][u"x520"]
1041         elif u"x710" in test_name:
1042             limit = plot[u"limits"][u"nic"][u"x710"]
1043         elif u"xxv710" in test_name:
1044             limit = plot[u"limits"][u"nic"][u"xxv710"]
1045         elif u"xl710" in test_name:
1046             limit = plot[u"limits"][u"nic"][u"xl710"]
1047         elif u"x553" in test_name:
1048             limit = plot[u"limits"][u"nic"][u"x553"]
1049         elif u"cx556a" in test_name:
1050             limit = plot[u"limits"][u"nic"][u"cx556a"]
1051         else:
1052             limit = 0
1053         if limit > nic_limit:
1054             nic_limit = limit
1055
1056         mul = 2 if u"ge2p" in test_name else 1
1057         if u"10ge" in test_name:
1058             limit = plot[u"limits"][u"link"][u"10ge"] * mul
1059         elif u"25ge" in test_name:
1060             limit = plot[u"limits"][u"link"][u"25ge"] * mul
1061         elif u"40ge" in test_name:
1062             limit = plot[u"limits"][u"link"][u"40ge"] * mul
1063         elif u"100ge" in test_name:
1064             limit = plot[u"limits"][u"link"][u"100ge"] * mul
1065         else:
1066             limit = 0
1067         if limit > lnk_limit:
1068             lnk_limit = limit
1069
1070     traces = list()
1071     annotations = list()
1072     x_vals = [1, 2, 4]
1073
1074     # Limits:
1075     try:
1076         threshold = 1.1 * max(y_max)  # 10%
1077     except ValueError as err:
1078         logging.error(err)
1079         return
1080     nic_limit /= 1e6
1081     traces.append(plgo.Scatter(
1082         x=x_vals,
1083         y=[nic_limit, ] * len(x_vals),
1084         name=f"NIC: {nic_limit:.2f}Mpps",
1085         showlegend=False,
1086         mode=u"lines",
1087         line=dict(
1088             dash=u"dot",
1089             color=COLORS[-1],
1090             width=1),
1091         hoverinfo=u"none"
1092     ))
1093     annotations.append(dict(
1094         x=1,
1095         y=nic_limit,
1096         xref=u"x",
1097         yref=u"y",
1098         xanchor=u"left",
1099         yanchor=u"bottom",
1100         text=f"NIC: {nic_limit:.2f}Mpps",
1101         font=dict(
1102             size=14,
1103             color=COLORS[-1],
1104         ),
1105         align=u"left",
1106         showarrow=False
1107     ))
1108     y_max.append(nic_limit)
1109
1110     lnk_limit /= 1e6
1111     if lnk_limit < threshold:
1112         traces.append(plgo.Scatter(
1113             x=x_vals,
1114             y=[lnk_limit, ] * len(x_vals),
1115             name=f"Link: {lnk_limit:.2f}Mpps",
1116             showlegend=False,
1117             mode=u"lines",
1118             line=dict(
1119                 dash=u"dot",
1120                 color=COLORS[-2],
1121                 width=1),
1122             hoverinfo=u"none"
1123         ))
1124         annotations.append(dict(
1125             x=1,
1126             y=lnk_limit,
1127             xref=u"x",
1128             yref=u"y",
1129             xanchor=u"left",
1130             yanchor=u"bottom",
1131             text=f"Link: {lnk_limit:.2f}Mpps",
1132             font=dict(
1133                 size=14,
1134                 color=COLORS[-2],
1135             ),
1136             align=u"left",
1137             showarrow=False
1138         ))
1139         y_max.append(lnk_limit)
1140
1141     pci_limit /= 1e6
1142     if (pci_limit < threshold and
1143             (pci_limit < lnk_limit * 0.95 or lnk_limit > lnk_limit * 1.05)):
1144         traces.append(plgo.Scatter(
1145             x=x_vals,
1146             y=[pci_limit, ] * len(x_vals),
1147             name=f"PCIe: {pci_limit:.2f}Mpps",
1148             showlegend=False,
1149             mode=u"lines",
1150             line=dict(
1151                 dash=u"dot",
1152                 color=COLORS[-3],
1153                 width=1),
1154             hoverinfo=u"none"
1155         ))
1156         annotations.append(dict(
1157             x=1,
1158             y=pci_limit,
1159             xref=u"x",
1160             yref=u"y",
1161             xanchor=u"left",
1162             yanchor=u"bottom",
1163             text=f"PCIe: {pci_limit:.2f}Mpps",
1164             font=dict(
1165                 size=14,
1166                 color=COLORS[-3],
1167             ),
1168             align=u"left",
1169             showarrow=False
1170         ))
1171         y_max.append(pci_limit)
1172
1173     # Perfect and measured:
1174     cidx = 0
1175     for name, val in vals.items():
1176         hovertext = list()
1177         try:
1178             for idx in range(len(val[u"val"])):
1179                 htext = ""
1180                 if isinstance(val[u"val"][idx], float):
1181                     htext += (
1182                         f"No. of Runs: {val[u'count'][idx]}<br>"
1183                         f"Mean: {val[u'val'][idx]:.2f}Mpps<br>"
1184                     )
1185                 if isinstance(val[u"diff"][idx], float):
1186                     htext += f"Diff: {round(val[u'diff'][idx]):.0f}%<br>"
1187                 if isinstance(val[u"rel"][idx], float):
1188                     htext += f"Speedup: {val[u'rel'][idx]:.2f}"
1189                 hovertext.append(htext)
1190             traces.append(
1191                 plgo.Scatter(
1192                     x=x_vals,
1193                     y=val[u"val"],
1194                     name=name,
1195                     legendgroup=name,
1196                     mode=u"lines+markers",
1197                     line=dict(
1198                         color=COLORS[cidx],
1199                         width=2),
1200                     marker=dict(
1201                         symbol=u"circle",
1202                         size=10
1203                     ),
1204                     text=hovertext,
1205                     hoverinfo=u"text+name"
1206                 )
1207             )
1208             traces.append(
1209                 plgo.Scatter(
1210                     x=x_vals,
1211                     y=val[u"ideal"],
1212                     name=f"{name} perfect",
1213                     legendgroup=name,
1214                     showlegend=False,
1215                     mode=u"lines",
1216                     line=dict(
1217                         color=COLORS[cidx],
1218                         width=2,
1219                         dash=u"dash"),
1220                     text=[f"Perfect: {y:.2f}Mpps" for y in val[u"ideal"]],
1221                     hoverinfo=u"text"
1222                 )
1223             )
1224             cidx += 1
1225         except (IndexError, ValueError, KeyError) as err:
1226             logging.warning(f"No data for {name}\n{repr(err)}")
1227
1228     try:
1229         # Create plot
1230         file_type = plot.get(u"output-file-type", u".html")
1231         logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
1232         layout = deepcopy(plot[u"layout"])
1233         if layout.get(u"title", None):
1234             layout[u"title"] = f"<b>Speedup Multi-core:</b> {layout[u'title']}"
1235         layout[u"yaxis"][u"range"] = [0, int(max(y_max) * 1.1)]
1236         layout[u"annotations"].extend(annotations)
1237         plpl = plgo.Figure(data=traces, layout=layout)
1238
1239         # Export Plot
1240         ploff.plot(
1241             plpl,
1242             show_link=False,
1243             auto_open=False,
1244             filename=f"{plot[u'output-file']}{file_type}"
1245         )
1246     except PlotlyError as err:
1247         logging.error(
1248             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1249         )
1250         return
1251
1252
1253 def plot_http_server_perf_box(plot, input_data):
1254     """Generate the plot(s) with algorithm: plot_http_server_perf_box
1255     specified in the specification file.
1256
1257     :param plot: Plot to generate.
1258     :param input_data: Data to process.
1259     :type plot: pandas.Series
1260     :type input_data: InputData
1261     """
1262
1263     # Transform the data
1264     logging.info(
1265         f"    Creating the data set for the {plot.get(u'type', u'')} "
1266         f"{plot.get(u'title', u'')}."
1267     )
1268     data = input_data.filter_data(plot)
1269     if data is None:
1270         logging.error(u"No data.")
1271         return
1272
1273     # Prepare the data for the plot
1274     y_vals = dict()
1275     for job in data:
1276         for build in job:
1277             for test in build:
1278                 if y_vals.get(test[u"name"], None) is None:
1279                     y_vals[test[u"name"]] = list()
1280                 try:
1281                     y_vals[test[u"name"]].append(test[u"result"])
1282                 except (KeyError, TypeError):
1283                     y_vals[test[u"name"]].append(None)
1284
1285     # Add None to the lists with missing data
1286     max_len = 0
1287     nr_of_samples = list()
1288     for val in y_vals.values():
1289         if len(val) > max_len:
1290             max_len = len(val)
1291         nr_of_samples.append(len(val))
1292     for val in y_vals.values():
1293         if len(val) < max_len:
1294             val.extend([None for _ in range(max_len - len(val))])
1295
1296     # Add plot traces
1297     traces = list()
1298     df_y = pd.DataFrame(y_vals)
1299     df_y.head()
1300     for i, col in enumerate(df_y.columns):
1301         name = \
1302             f"{i + 1}. " \
1303             f"({nr_of_samples[i]:02d} " \
1304             f"run{u's' if nr_of_samples[i] > 1 else u''}) " \
1305             f"{col.lower().replace(u'-ndrpdr', u'')}"
1306         if len(name) > 50:
1307             name_lst = name.split(u'-')
1308             name = u""
1309             split_name = True
1310             for segment in name_lst:
1311                 if (len(name) + len(segment) + 1) > 50 and split_name:
1312                     name += u"<br>    "
1313                     split_name = False
1314                 name += segment + u'-'
1315             name = name[:-1]
1316
1317         traces.append(plgo.Box(x=[str(i + 1) + u'.'] * len(df_y[col]),
1318                                y=df_y[col],
1319                                name=name,
1320                                **plot[u"traces"]))
1321     try:
1322         # Create plot
1323         plpl = plgo.Figure(data=traces, layout=plot[u"layout"])
1324
1325         # Export Plot
1326         logging.info(
1327             f"    Writing file {plot[u'output-file']}"
1328             f"{plot[u'output-file-type']}."
1329         )
1330         ploff.plot(
1331             plpl,
1332             show_link=False,
1333             auto_open=False,
1334             filename=f"{plot[u'output-file']}{plot[u'output-file-type']}"
1335         )
1336     except PlotlyError as err:
1337         logging.error(
1338             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1339         )
1340         return
1341
1342
1343 def plot_nf_heatmap(plot, input_data):
1344     """Generate the plot(s) with algorithm: plot_nf_heatmap
1345     specified in the specification file.
1346
1347     :param plot: Plot to generate.
1348     :param input_data: Data to process.
1349     :type plot: pandas.Series
1350     :type input_data: InputData
1351     """
1352
1353     regex_cn = re.compile(r'^(\d*)R(\d*)C$')
1354     regex_test_name = re.compile(r'^.*-(\d+ch|\d+pl)-'
1355                                  r'(\d+mif|\d+vh)-'
1356                                  r'(\d+vm\d+t|\d+dcr\d+t|\d+dcr\d+c).*$')
1357     vals = dict()
1358
1359     # Transform the data
1360     logging.info(
1361         f"    Creating the data set for the {plot.get(u'type', u'')} "
1362         f"{plot.get(u'title', u'')}."
1363     )
1364     data = input_data.filter_data(plot, continue_on_error=True)
1365     if data is None or data.empty:
1366         logging.error(u"No data.")
1367         return
1368
1369     for job in data:
1370         for build in job:
1371             for test in build:
1372                 for tag in test[u"tags"]:
1373                     groups = re.search(regex_cn, tag)
1374                     if groups:
1375                         chain = str(groups.group(1))
1376                         node = str(groups.group(2))
1377                         break
1378                 else:
1379                     continue
1380                 groups = re.search(regex_test_name, test[u"name"])
1381                 if groups and len(groups.groups()) == 3:
1382                     hover_name = (
1383                         f"{str(groups.group(1))}-"
1384                         f"{str(groups.group(2))}-"
1385                         f"{str(groups.group(3))}"
1386                     )
1387                 else:
1388                     hover_name = u""
1389                 if vals.get(chain, None) is None:
1390                     vals[chain] = dict()
1391                 if vals[chain].get(node, None) is None:
1392                     vals[chain][node] = dict(
1393                         name=hover_name,
1394                         vals=list(),
1395                         nr=None,
1396                         mean=None,
1397                         stdev=None
1398                     )
1399                 try:
1400                     if plot[u"include-tests"] == u"MRR":
1401                         result = test[u"result"][u"receive-rate"]
1402                     elif plot[u"include-tests"] == u"PDR":
1403                         result = test[u"throughput"][u"PDR"][u"LOWER"]
1404                     elif plot[u"include-tests"] == u"NDR":
1405                         result = test[u"throughput"][u"NDR"][u"LOWER"]
1406                     else:
1407                         result = None
1408                 except TypeError:
1409                     result = None
1410
1411                 if result:
1412                     vals[chain][node][u"vals"].append(result)
1413
1414     if not vals:
1415         logging.error(u"No data.")
1416         return
1417
1418     txt_chains = list()
1419     txt_nodes = list()
1420     for key_c in vals:
1421         txt_chains.append(key_c)
1422         for key_n in vals[key_c].keys():
1423             txt_nodes.append(key_n)
1424             if vals[key_c][key_n][u"vals"]:
1425                 vals[key_c][key_n][u"nr"] = len(vals[key_c][key_n][u"vals"])
1426                 vals[key_c][key_n][u"mean"] = \
1427                     round(mean(vals[key_c][key_n][u"vals"]) / 1000000, 1)
1428                 vals[key_c][key_n][u"stdev"] = \
1429                     round(stdev(vals[key_c][key_n][u"vals"]) / 1000000, 1)
1430     txt_nodes = list(set(txt_nodes))
1431
1432     def sort_by_int(value):
1433         """Makes possible to sort a list of strings which represent integers.
1434
1435         :param value: Integer as a string.
1436         :type value: str
1437         :returns: Integer representation of input parameter 'value'.
1438         :rtype: int
1439         """
1440         return int(value)
1441
1442     txt_chains = sorted(txt_chains, key=sort_by_int)
1443     txt_nodes = sorted(txt_nodes, key=sort_by_int)
1444
1445     chains = [i + 1 for i in range(len(txt_chains))]
1446     nodes = [i + 1 for i in range(len(txt_nodes))]
1447
1448     data = [list() for _ in range(len(chains))]
1449     for chain in chains:
1450         for node in nodes:
1451             try:
1452                 val = vals[txt_chains[chain - 1]][txt_nodes[node - 1]][u"mean"]
1453             except (KeyError, IndexError):
1454                 val = None
1455             data[chain - 1].append(val)
1456
1457     # Color scales:
1458     my_green = [[0.0, u"rgb(235, 249, 242)"],
1459                 [1.0, u"rgb(45, 134, 89)"]]
1460
1461     my_blue = [[0.0, u"rgb(236, 242, 248)"],
1462                [1.0, u"rgb(57, 115, 172)"]]
1463
1464     my_grey = [[0.0, u"rgb(230, 230, 230)"],
1465                [1.0, u"rgb(102, 102, 102)"]]
1466
1467     hovertext = list()
1468     annotations = list()
1469
1470     text = (u"Test: {name}<br>"
1471             u"Runs: {nr}<br>"
1472             u"Thput: {val}<br>"
1473             u"StDev: {stdev}")
1474
1475     for chain, _ in enumerate(txt_chains):
1476         hover_line = list()
1477         for node, _ in enumerate(txt_nodes):
1478             if data[chain][node] is not None:
1479                 annotations.append(
1480                     dict(
1481                         x=node+1,
1482                         y=chain+1,
1483                         xref=u"x",
1484                         yref=u"y",
1485                         xanchor=u"center",
1486                         yanchor=u"middle",
1487                         text=str(data[chain][node]),
1488                         font=dict(
1489                             size=14,
1490                         ),
1491                         align=u"center",
1492                         showarrow=False
1493                     )
1494                 )
1495                 hover_line.append(text.format(
1496                     name=vals[txt_chains[chain]][txt_nodes[node]][u"name"],
1497                     nr=vals[txt_chains[chain]][txt_nodes[node]][u"nr"],
1498                     val=data[chain][node],
1499                     stdev=vals[txt_chains[chain]][txt_nodes[node]][u"stdev"]))
1500         hovertext.append(hover_line)
1501
1502     traces = [
1503         plgo.Heatmap(
1504             x=nodes,
1505             y=chains,
1506             z=data,
1507             colorbar=dict(
1508                 title=plot.get(u"z-axis", u""),
1509                 titleside=u"right",
1510                 titlefont=dict(
1511                     size=16
1512                 ),
1513                 tickfont=dict(
1514                     size=16,
1515                 ),
1516                 tickformat=u".1f",
1517                 yanchor=u"bottom",
1518                 y=-0.02,
1519                 len=0.925,
1520             ),
1521             showscale=True,
1522             colorscale=my_green,
1523             text=hovertext,
1524             hoverinfo=u"text"
1525         )
1526     ]
1527
1528     for idx, item in enumerate(txt_nodes):
1529         # X-axis, numbers:
1530         annotations.append(
1531             dict(
1532                 x=idx+1,
1533                 y=0.05,
1534                 xref=u"x",
1535                 yref=u"y",
1536                 xanchor=u"center",
1537                 yanchor=u"top",
1538                 text=item,
1539                 font=dict(
1540                     size=16,
1541                 ),
1542                 align=u"center",
1543                 showarrow=False
1544             )
1545         )
1546     for idx, item in enumerate(txt_chains):
1547         # Y-axis, numbers:
1548         annotations.append(
1549             dict(
1550                 x=0.35,
1551                 y=idx+1,
1552                 xref=u"x",
1553                 yref=u"y",
1554                 xanchor=u"right",
1555                 yanchor=u"middle",
1556                 text=item,
1557                 font=dict(
1558                     size=16,
1559                 ),
1560                 align=u"center",
1561                 showarrow=False
1562             )
1563         )
1564     # X-axis, title:
1565     annotations.append(
1566         dict(
1567             x=0.55,
1568             y=-0.15,
1569             xref=u"paper",
1570             yref=u"y",
1571             xanchor=u"center",
1572             yanchor=u"bottom",
1573             text=plot.get(u"x-axis", u""),
1574             font=dict(
1575                 size=16,
1576             ),
1577             align=u"center",
1578             showarrow=False
1579         )
1580     )
1581     # Y-axis, title:
1582     annotations.append(
1583         dict(
1584             x=-0.1,
1585             y=0.5,
1586             xref=u"x",
1587             yref=u"paper",
1588             xanchor=u"center",
1589             yanchor=u"middle",
1590             text=plot.get(u"y-axis", u""),
1591             font=dict(
1592                 size=16,
1593             ),
1594             align=u"center",
1595             textangle=270,
1596             showarrow=False
1597         )
1598     )
1599     updatemenus = list([
1600         dict(
1601             x=1.0,
1602             y=0.0,
1603             xanchor=u"right",
1604             yanchor=u"bottom",
1605             direction=u"up",
1606             buttons=list([
1607                 dict(
1608                     args=[
1609                         {
1610                             u"colorscale": [my_green, ],
1611                             u"reversescale": False
1612                         }
1613                     ],
1614                     label=u"Green",
1615                     method=u"update"
1616                 ),
1617                 dict(
1618                     args=[
1619                         {
1620                             u"colorscale": [my_blue, ],
1621                             u"reversescale": False
1622                         }
1623                     ],
1624                     label=u"Blue",
1625                     method=u"update"
1626                 ),
1627                 dict(
1628                     args=[
1629                         {
1630                             u"colorscale": [my_grey, ],
1631                             u"reversescale": False
1632                         }
1633                     ],
1634                     label=u"Grey",
1635                     method=u"update"
1636                 )
1637             ])
1638         )
1639     ])
1640
1641     try:
1642         layout = deepcopy(plot[u"layout"])
1643     except KeyError as err:
1644         logging.error(f"Finished with error: No layout defined\n{repr(err)}")
1645         return
1646
1647     layout[u"annotations"] = annotations
1648     layout[u'updatemenus'] = updatemenus
1649
1650     try:
1651         # Create plot
1652         plpl = plgo.Figure(data=traces, layout=layout)
1653
1654         # Export Plot
1655         logging.info(f"    Writing file {plot[u'output-file']}.html")
1656         ploff.plot(
1657             plpl,
1658             show_link=False,
1659             auto_open=False,
1660             filename=f"{plot[u'output-file']}.html"
1661         )
1662     except PlotlyError as err:
1663         logging.error(
1664             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1665         )
1666         return