Report: Configure rls2101.09
[csit.git] / resources / tools / presentation / generator_plots.py
1 # Copyright (c) 2021 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 import hdrh.histogram
22 import hdrh.codec
23 import pandas as pd
24 import plotly.offline as ploff
25 import plotly.graph_objs as plgo
26
27 from collections import OrderedDict
28 from copy import deepcopy
29 from math import log
30
31 from plotly.exceptions import PlotlyError
32
33 from pal_utils import mean, stdev
34
35
36 COLORS = (
37     u"#1A1110",
38     u"#DA2647",
39     u"#214FC6",
40     u"#01786F",
41     u"#BD8260",
42     u"#FFD12A",
43     u"#A6E7FF",
44     u"#738276",
45     u"#C95A49",
46     u"#FC5A8D",
47     u"#CEC8EF",
48     u"#391285",
49     u"#6F2DA8",
50     u"#FF878D",
51     u"#45A27D",
52     u"#FFD0B9",
53     u"#FD5240",
54     u"#DB91EF",
55     u"#44D7A8",
56     u"#4F86F7",
57     u"#84DE02",
58     u"#FFCFF1",
59     u"#614051"
60 )
61
62 REGEX_NIC = re.compile(r'(\d*ge\dp\d\D*\d*[a-z]*)-')
63
64 # This value depends on latency stream rate (9001 pps) and duration (5s).
65 # Keep it slightly higher to ensure rounding errors to not remove tick mark.
66 PERCENTILE_MAX = 99.999501
67
68
69 def generate_plots(spec, data):
70     """Generate all plots specified in the specification file.
71
72     :param spec: Specification read from the specification file.
73     :param data: Data to process.
74     :type spec: Specification
75     :type data: InputData
76     """
77
78     generator = {
79         u"plot_nf_reconf_box_name": plot_nf_reconf_box_name,
80         u"plot_perf_box_name": plot_perf_box_name,
81         u"plot_tsa_name": plot_tsa_name,
82         u"plot_http_server_perf_box": plot_http_server_perf_box,
83         u"plot_nf_heatmap": plot_nf_heatmap,
84         u"plot_hdrh_lat_by_percentile": plot_hdrh_lat_by_percentile,
85         u"plot_hdrh_lat_by_percentile_x_log": plot_hdrh_lat_by_percentile_x_log,
86         u"plot_mrr_error_bars_name": plot_mrr_error_bars_name,
87         u"plot_mrr_box_name": plot_mrr_box_name
88     }
89
90     logging.info(u"Generating the plots ...")
91     for index, plot in enumerate(spec.plots):
92         try:
93             logging.info(f"  Plot nr {index + 1}: {plot.get(u'title', u'')}")
94             plot[u"limits"] = spec.configuration[u"limits"]
95             generator[plot[u"algorithm"]](plot, data)
96             logging.info(u"  Done.")
97         except NameError as err:
98             logging.error(
99                 f"Probably algorithm {plot[u'algorithm']} is not defined: "
100                 f"{repr(err)}"
101             )
102     logging.info(u"Done.")
103
104
105 def plot_hdrh_lat_by_percentile(plot, input_data):
106     """Generate the plot(s) with algorithm: plot_hdrh_lat_by_percentile
107     specified in the specification file.
108
109     :param plot: Plot to generate.
110     :param input_data: Data to process.
111     :type plot: pandas.Series
112     :type input_data: InputData
113     """
114
115     # Transform the data
116     logging.info(
117         f"    Creating the data set for the {plot.get(u'type', u'')} "
118         f"{plot.get(u'title', u'')}."
119     )
120     if plot.get(u"include", None):
121         data = input_data.filter_tests_by_name(
122             plot,
123             params=[u"name", u"latency", u"parent", u"tags", u"type"]
124         )[0][0]
125     elif plot.get(u"filter", None):
126         data = input_data.filter_data(
127             plot,
128             params=[u"name", u"latency", u"parent", u"tags", u"type"],
129             continue_on_error=True
130         )[0][0]
131     else:
132         job = list(plot[u"data"].keys())[0]
133         build = str(plot[u"data"][job][0])
134         data = input_data.tests(job, build)
135
136     if data is None or len(data) == 0:
137         logging.error(u"No data.")
138         return
139
140     desc = {
141         u"LAT0": u"No-load.",
142         u"PDR10": u"Low-load, 10% PDR.",
143         u"PDR50": u"Mid-load, 50% PDR.",
144         u"PDR90": u"High-load, 90% PDR.",
145         u"PDR": u"Full-load, 100% PDR.",
146         u"NDR10": u"Low-load, 10% NDR.",
147         u"NDR50": u"Mid-load, 50% NDR.",
148         u"NDR90": u"High-load, 90% NDR.",
149         u"NDR": u"Full-load, 100% NDR."
150     }
151
152     graphs = [
153         u"LAT0",
154         u"PDR10",
155         u"PDR50",
156         u"PDR90"
157     ]
158
159     file_links = plot.get(u"output-file-links", None)
160     target_links = plot.get(u"target-links", None)
161
162     for test in data:
163         try:
164             if test[u"type"] not in (u"NDRPDR",):
165                 logging.warning(f"Invalid test type: {test[u'type']}")
166                 continue
167             name = re.sub(REGEX_NIC, u"", test[u"parent"].
168                           replace(u'-ndrpdr', u'').replace(u'2n1l-', u''))
169             try:
170                 nic = re.search(REGEX_NIC, test[u"parent"]).group(1)
171             except (IndexError, AttributeError, KeyError, ValueError):
172                 nic = u""
173             name_link = f"{nic}-{test[u'name']}".replace(u'-ndrpdr', u'')
174
175             logging.info(f"    Generating the graph: {name_link}")
176
177             fig = plgo.Figure()
178             layout = deepcopy(plot[u"layout"])
179
180             for color, graph in enumerate(graphs):
181                 for idx, direction in enumerate((u"direction1", u"direction2")):
182                     previous_x = 0.0
183                     xaxis = list()
184                     yaxis = list()
185                     hovertext = list()
186                     try:
187                         decoded = hdrh.histogram.HdrHistogram.decode(
188                             test[u"latency"][graph][direction][u"hdrh"]
189                         )
190                     except hdrh.codec.HdrLengthException:
191                         logging.warning(
192                             f"No data for direction {(u'W-E', u'E-W')[idx % 2]}"
193                         )
194                         continue
195
196                     for item in decoded.get_recorded_iterator():
197                         percentile = item.percentile_level_iterated_to
198                         xaxis.append(previous_x)
199                         yaxis.append(item.value_iterated_to)
200                         hovertext.append(
201                             f"<b>{desc[graph]}</b><br>"
202                             f"Direction: {(u'W-E', u'E-W')[idx % 2]}<br>"
203                             f"Percentile: {previous_x:.5f}-{percentile:.5f}%<br>"
204                             f"Latency: {item.value_iterated_to}uSec"
205                         )
206                         xaxis.append(percentile)
207                         yaxis.append(item.value_iterated_to)
208                         hovertext.append(
209                             f"<b>{desc[graph]}</b><br>"
210                             f"Direction: {(u'W-E', u'E-W')[idx % 2]}<br>"
211                             f"Percentile: {previous_x:.5f}-{percentile:.5f}%<br>"
212                             f"Latency: {item.value_iterated_to}uSec"
213                         )
214                         previous_x = percentile
215                     fig.add_trace(
216                         plgo.Scatter(
217                             x=xaxis,
218                             y=yaxis,
219                             name=desc[graph],
220                             mode=u"lines",
221                             legendgroup=desc[graph],
222                             showlegend=bool(idx),
223                             line=dict(
224                                 color=COLORS[color],
225                                 dash=u"solid",
226                                 width=1 if idx % 2 else 2
227                             ),
228                             hovertext=hovertext,
229                             hoverinfo=u"text"
230                         )
231                     )
232
233             layout[u"title"][u"text"] = f"<b>Latency:</b> {name}"
234             fig.update_layout(layout)
235
236             # Create plot
237             file_name = f"{plot[u'output-file']}-{name_link}.html"
238             logging.info(f"    Writing file {file_name}")
239
240             try:
241                 # Export Plot
242                 ploff.plot(fig, show_link=False, auto_open=False,
243                            filename=file_name)
244                 # Add link to the file:
245                 if file_links and target_links:
246                     with open(file_links, u"a") as file_handler:
247                         file_handler.write(
248                             f"- `{name_link} "
249                             f"<{target_links}/{file_name.split(u'/')[-1]}>`_\n"
250                         )
251             except FileNotFoundError as err:
252                 logging.error(
253                     f"Not possible to write the link to the file "
254                     f"{file_links}\n{err}"
255                 )
256             except PlotlyError as err:
257                 logging.error(f"   Finished with error: {repr(err)}")
258
259         except hdrh.codec.HdrLengthException as err:
260             logging.warning(repr(err))
261             continue
262
263         except (ValueError, KeyError) as err:
264             logging.warning(repr(err))
265             continue
266
267
268 def plot_hdrh_lat_by_percentile_x_log(plot, input_data):
269     """Generate the plot(s) with algorithm: plot_hdrh_lat_by_percentile_x_log
270     specified in the specification file.
271
272     :param plot: Plot to generate.
273     :param input_data: Data to process.
274     :type plot: pandas.Series
275     :type input_data: InputData
276     """
277
278     # Transform the data
279     logging.info(
280         f"    Creating the data set for the {plot.get(u'type', u'')} "
281         f"{plot.get(u'title', u'')}."
282     )
283     if plot.get(u"include", None):
284         data = input_data.filter_tests_by_name(
285             plot,
286             params=[u"name", u"latency", u"parent", u"tags", u"type"]
287         )[0][0]
288     elif plot.get(u"filter", None):
289         data = input_data.filter_data(
290             plot,
291             params=[u"name", u"latency", u"parent", u"tags", u"type"],
292             continue_on_error=True
293         )[0][0]
294     else:
295         job = list(plot[u"data"].keys())[0]
296         build = str(plot[u"data"][job][0])
297         data = input_data.tests(job, build)
298
299     if data is None or len(data) == 0:
300         logging.error(u"No data.")
301         return
302
303     desc = {
304         u"LAT0": u"No-load.",
305         u"PDR10": u"Low-load, 10% PDR.",
306         u"PDR50": u"Mid-load, 50% PDR.",
307         u"PDR90": u"High-load, 90% PDR.",
308         u"PDR": u"Full-load, 100% PDR.",
309         u"NDR10": u"Low-load, 10% NDR.",
310         u"NDR50": u"Mid-load, 50% NDR.",
311         u"NDR90": u"High-load, 90% NDR.",
312         u"NDR": u"Full-load, 100% NDR."
313     }
314
315     graphs = [
316         u"LAT0",
317         u"PDR10",
318         u"PDR50",
319         u"PDR90"
320     ]
321
322     file_links = plot.get(u"output-file-links", None)
323     target_links = plot.get(u"target-links", None)
324
325     for test in data:
326         try:
327             if test[u"type"] not in (u"NDRPDR",):
328                 logging.warning(f"Invalid test type: {test[u'type']}")
329                 continue
330             name = re.sub(REGEX_NIC, u"", test[u"parent"].
331                           replace(u'-ndrpdr', u'').replace(u'2n1l-', u''))
332             try:
333                 nic = re.search(REGEX_NIC, test[u"parent"]).group(1)
334             except (IndexError, AttributeError, KeyError, ValueError):
335                 nic = u""
336             name_link = f"{nic}-{test[u'name']}".replace(u'-ndrpdr', u'')
337
338             logging.info(f"    Generating the graph: {name_link}")
339
340             fig = plgo.Figure()
341             layout = deepcopy(plot[u"layout"])
342
343             for color, graph in enumerate(graphs):
344                 for idx, direction in enumerate((u"direction1", u"direction2")):
345                     previous_x = 0.0
346                     prev_perc = 0.0
347                     xaxis = list()
348                     yaxis = list()
349                     hovertext = list()
350                     try:
351                         decoded = hdrh.histogram.HdrHistogram.decode(
352                             test[u"latency"][graph][direction][u"hdrh"]
353                         )
354                     except hdrh.codec.HdrLengthException:
355                         logging.warning(
356                             f"No data for direction {(u'W-E', u'E-W')[idx % 2]}"
357                         )
358                         continue
359
360                     for item in decoded.get_recorded_iterator():
361                         # The real value is "percentile".
362                         # For 100%, we cut that down to "x_perc" to avoid
363                         # infinity.
364                         percentile = item.percentile_level_iterated_to
365                         x_perc = min(percentile, PERCENTILE_MAX)
366                         xaxis.append(previous_x)
367                         yaxis.append(item.value_iterated_to)
368                         hovertext.append(
369                             f"<b>{desc[graph]}</b><br>"
370                             f"Direction: {(u'W-E', u'E-W')[idx % 2]}<br>"
371                             f"Percentile: {prev_perc:.5f}-{percentile:.5f}%<br>"
372                             f"Latency: {item.value_iterated_to}uSec"
373                         )
374                         next_x = 100.0 / (100.0 - x_perc)
375                         xaxis.append(next_x)
376                         yaxis.append(item.value_iterated_to)
377                         hovertext.append(
378                             f"<b>{desc[graph]}</b><br>"
379                             f"Direction: {(u'W-E', u'E-W')[idx % 2]}<br>"
380                             f"Percentile: {prev_perc:.5f}-{percentile:.5f}%<br>"
381                             f"Latency: {item.value_iterated_to}uSec"
382                         )
383                         previous_x = next_x
384                         prev_perc = percentile
385                     fig.add_trace(
386                         plgo.Scatter(
387                             x=xaxis,
388                             y=yaxis,
389                             name=desc[graph],
390                             mode=u"lines",
391                             legendgroup=desc[graph],
392                             showlegend=not(bool(idx)),
393                             line=dict(
394                                 color=COLORS[color],
395                                 dash=u"solid",
396                                 width=1 if idx % 2 else 2
397                             ),
398                             hovertext=hovertext,
399                             hoverinfo=u"text"
400                         )
401                     )
402
403             layout[u"title"][u"text"] = f"<b>Latency:</b> {name}"
404             x_max = log(100.0 / (100.0 - PERCENTILE_MAX), 10)
405             layout[u"xaxis"][u"range"] = [0, x_max]
406             fig.update_layout(layout)
407
408             # Create plot
409             file_name = f"{plot[u'output-file']}-{name_link}.html"
410             logging.info(f"    Writing file {file_name}")
411
412             try:
413                 # Export Plot
414                 ploff.plot(fig, show_link=False, auto_open=False,
415                            filename=file_name)
416                 # Add link to the file:
417                 if file_links and target_links:
418                     with open(file_links, u"a") as file_handler:
419                         file_handler.write(
420                             f"- `{name_link} "
421                             f"<{target_links}/{file_name.split(u'/')[-1]}>`_\n"
422                         )
423             except FileNotFoundError as err:
424                 logging.error(
425                     f"Not possible to write the link to the file "
426                     f"{file_links}\n{err}"
427                 )
428             except PlotlyError as err:
429                 logging.error(f"   Finished with error: {repr(err)}")
430
431         except hdrh.codec.HdrLengthException as err:
432             logging.warning(repr(err))
433             continue
434
435         except (ValueError, KeyError) as err:
436             logging.warning(repr(err))
437             continue
438
439
440 def plot_nf_reconf_box_name(plot, input_data):
441     """Generate the plot(s) with algorithm: plot_nf_reconf_box_name
442     specified in the specification file.
443
444     :param plot: Plot to generate.
445     :param input_data: Data to process.
446     :type plot: pandas.Series
447     :type input_data: InputData
448     """
449
450     # Transform the data
451     logging.info(
452         f"    Creating the data set for the {plot.get(u'type', u'')} "
453         f"{plot.get(u'title', u'')}."
454     )
455     data = input_data.filter_tests_by_name(
456         plot, params=[u"result", u"parent", u"tags", u"type"]
457     )
458     if data is None:
459         logging.error(u"No data.")
460         return
461
462     # Prepare the data for the plot
463     y_vals = OrderedDict()
464     loss = dict()
465     for job in data:
466         for build in job:
467             for test in build:
468                 if y_vals.get(test[u"parent"], None) is None:
469                     y_vals[test[u"parent"]] = list()
470                     loss[test[u"parent"]] = list()
471                 try:
472                     y_vals[test[u"parent"]].append(test[u"result"][u"time"])
473                     loss[test[u"parent"]].append(test[u"result"][u"loss"])
474                 except (KeyError, TypeError):
475                     y_vals[test[u"parent"]].append(None)
476
477     # Add None to the lists with missing data
478     max_len = 0
479     nr_of_samples = list()
480     for val in y_vals.values():
481         if len(val) > max_len:
482             max_len = len(val)
483         nr_of_samples.append(len(val))
484     for val in y_vals.values():
485         if len(val) < max_len:
486             val.extend([None for _ in range(max_len - len(val))])
487
488     # Add plot traces
489     traces = list()
490     df_y = pd.DataFrame(y_vals)
491     df_y.head()
492     for i, col in enumerate(df_y.columns):
493
494         tst_name = re.sub(REGEX_NIC, u"",
495                           col.lower().replace(u'-reconf', u'').
496                           replace(u'2n1l-', u'').replace(u'2n-', u'').
497                           replace(u'-testpmd', u''))
498
499         traces.append(plgo.Box(
500             x=[str(i + 1) + u'.'] * len(df_y[col]),
501             y=df_y[col],
502             name=(
503                 f"{i + 1}. "
504                 f"({nr_of_samples[i]:02d} "
505                 f"run{u's' if nr_of_samples[i] > 1 else u''}, "
506                 f"packets lost average: {mean(loss[col]):.1f}) "
507                 f"{u'-'.join(tst_name.split(u'-')[2:])}"
508             ),
509             hoverinfo=u"y+name"
510         ))
511     try:
512         # Create plot
513         layout = deepcopy(plot[u"layout"])
514         layout[u"title"] = f"<b>Time Lost:</b> {layout[u'title']}"
515         layout[u"yaxis"][u"title"] = u"<b>Effective Blocked Time [s]</b>"
516         layout[u"legend"][u"font"][u"size"] = 14
517         layout[u"yaxis"].pop(u"range")
518         plpl = plgo.Figure(data=traces, layout=layout)
519
520         # Export Plot
521         file_type = plot.get(u"output-file-type", u".html")
522         logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
523         ploff.plot(
524             plpl,
525             show_link=False,
526             auto_open=False,
527             filename=f"{plot[u'output-file']}{file_type}"
528         )
529     except PlotlyError as err:
530         logging.error(
531             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
532         )
533         return
534
535
536 def plot_perf_box_name(plot, input_data):
537     """Generate the plot(s) with algorithm: plot_perf_box_name
538     specified in the specification file.
539
540     :param plot: Plot to generate.
541     :param input_data: Data to process.
542     :type plot: pandas.Series
543     :type input_data: InputData
544     """
545
546     # Transform the data
547     logging.info(
548         f"    Creating data set for the {plot.get(u'type', u'')} "
549         f"{plot.get(u'title', u'')}."
550     )
551     data = input_data.filter_tests_by_name(
552         plot,
553         params=[u"throughput", u"gbps", u"result", u"parent", u"tags", u"type"])
554     if data is None:
555         logging.error(u"No data.")
556         return
557
558     # Prepare the data for the plot
559     plot_title = plot.get(u"title", u"").lower()
560
561     if u"-gbps" in plot_title:
562         value = u"gbps"
563         multiplier = 1e6
564     else:
565         value = u"throughput"
566         multiplier = 1.0
567     y_vals = OrderedDict()
568     test_type = u""
569
570     for item in plot.get(u"include", tuple()):
571         reg_ex = re.compile(str(item).lower())
572         for job in data:
573             for build in job:
574                 for test_id, test in build.iteritems():
575                     if not re.match(reg_ex, str(test_id).lower()):
576                         continue
577                     if y_vals.get(test[u"parent"], None) is None:
578                         y_vals[test[u"parent"]] = list()
579                     try:
580                         if test[u"type"] in (u"NDRPDR", u"CPS"):
581                             test_type = test[u"type"]
582
583                             if u"-pdr" in plot_title:
584                                 ttype = u"PDR"
585                             elif u"-ndr" in plot_title:
586                                 ttype = u"NDR"
587                             else:
588                                 raise RuntimeError(
589                                     u"Wrong title. No information about test "
590                                     u"type. Add '-ndr' or '-pdr' to the test "
591                                     u"title."
592                                 )
593
594                             y_vals[test[u"parent"]].append(
595                                 test[value][ttype][u"LOWER"] * multiplier
596                             )
597
598                         elif test[u"type"] in (u"SOAK",):
599                             y_vals[test[u"parent"]]. \
600                                 append(test[u"throughput"][u"LOWER"])
601                             test_type = u"SOAK"
602
603                         elif test[u"type"] in (u"HOSTSTACK",):
604                             if u"LDPRELOAD" in test[u"tags"]:
605                                 y_vals[test[u"parent"]].append(
606                                     float(
607                                         test[u"result"][u"bits_per_second"]
608                                     ) / 1e3
609                                 )
610                             elif u"VPPECHO" in test[u"tags"]:
611                                 y_vals[test[u"parent"]].append(
612                                     (float(
613                                         test[u"result"][u"client"][u"tx_data"]
614                                     ) * 8 / 1e3) /
615                                     ((float(
616                                         test[u"result"][u"client"][u"time"]
617                                     ) +
618                                       float(
619                                           test[u"result"][u"server"][u"time"])
620                                       ) / 2)
621                                 )
622                             test_type = u"HOSTSTACK"
623
624                         else:
625                             continue
626
627                     except (KeyError, TypeError):
628                         y_vals[test[u"parent"]].append(None)
629
630     # Add None to the lists with missing data
631     max_len = 0
632     nr_of_samples = list()
633     for val in y_vals.values():
634         if len(val) > max_len:
635             max_len = len(val)
636         nr_of_samples.append(len(val))
637     for val in y_vals.values():
638         if len(val) < max_len:
639             val.extend([None for _ in range(max_len - len(val))])
640
641     # Add plot traces
642     traces = list()
643     df_y = pd.DataFrame(y_vals)
644     df_y.head()
645     y_max = list()
646     for i, col in enumerate(df_y.columns):
647         tst_name = re.sub(REGEX_NIC, u"",
648                           col.lower().replace(u'-ndrpdr', u'').
649                           replace(u'2n1l-', u''))
650         kwargs = dict(
651             x=[str(i + 1) + u'.'] * len(df_y[col]),
652             y=[y / 1e6 if y else None for y in df_y[col]],
653             name=(
654                 f"{i + 1}. "
655                 f"({nr_of_samples[i]:02d} "
656                 f"run{u's' if nr_of_samples[i] > 1 else u''}) "
657                 f"{tst_name}"
658             ),
659             hoverinfo=u"y+name"
660         )
661         if test_type in (u"SOAK", ):
662             kwargs[u"boxpoints"] = u"all"
663
664         traces.append(plgo.Box(**kwargs))
665
666         try:
667             val_max = max(df_y[col])
668             if val_max:
669                 y_max.append(int(val_max / 1e6) + 2)
670         except (ValueError, TypeError) as err:
671             logging.error(repr(err))
672             continue
673
674     try:
675         # Create plot
676         layout = deepcopy(plot[u"layout"])
677         if layout.get(u"title", None):
678             if test_type in (u"HOSTSTACK", ):
679                 layout[u"title"] = f"<b>Bandwidth:</b> {layout[u'title']}"
680             elif test_type in (u"CPS", ):
681                 layout[u"title"] = f"<b>CPS:</b> {layout[u'title']}"
682             else:
683                 layout[u"title"] = f"<b>Throughput:</b> {layout[u'title']}"
684         if y_max:
685             layout[u"yaxis"][u"range"] = [0, max(y_max)]
686         plpl = plgo.Figure(data=traces, layout=layout)
687
688         # Export Plot
689         logging.info(f"    Writing file {plot[u'output-file']}.html.")
690         ploff.plot(
691             plpl,
692             show_link=False,
693             auto_open=False,
694             filename=f"{plot[u'output-file']}.html"
695         )
696     except PlotlyError as err:
697         logging.error(
698             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
699         )
700         return
701
702
703 def plot_mrr_box_name(plot, input_data):
704     """Generate the plot(s) with algorithm: plot_mrr_box_name
705     specified in the specification file.
706
707     :param plot: Plot to generate.
708     :param input_data: Data to process.
709     :type plot: pandas.Series
710     :type input_data: InputData
711     """
712
713     # Transform the data
714     logging.info(
715         f"    Creating data set for the {plot.get(u'type', u'')} "
716         f"{plot.get(u'title', u'')}."
717     )
718     data = input_data.filter_tests_by_name(
719         plot,
720         params=[u"result", u"parent", u"tags", u"type"])
721     if data is None:
722         logging.error(u"No data.")
723         return
724
725     # Prepare the data for the plot
726     data_x = list()
727     data_names = list()
728     data_y = list()
729     data_y_max = list()
730     idx = 1
731     for item in plot.get(u"include", tuple()):
732         reg_ex = re.compile(str(item).lower())
733         for job in data:
734             for build in job:
735                 for test_id, test in build.iteritems():
736                     if not re.match(reg_ex, str(test_id).lower()):
737                         continue
738                     try:
739                         data_x.append(idx)
740                         name = re.sub(REGEX_NIC, u'', test[u'parent'].lower().
741                                       replace(u'-mrr', u'').
742                                       replace(u'2n1l-', u''))
743                         data_y.append(test[u"result"][u"samples"])
744                         data_names.append(
745                             f"{idx}."
746                             f"({len(data_y[-1]):02d} "
747                             f"run{u's' if len(data_y[-1]) > 1 else u''}) "
748                             f"{name}"
749                         )
750                         data_y_max.append(max(test[u"result"][u"samples"]))
751                         idx += 1
752                     except (KeyError, TypeError):
753                         pass
754
755     # Add plot traces
756     traces = list()
757     for idx in range(len(data_x)):
758         traces.append(
759             plgo.Box(
760                 x=[data_x[idx], ] * len(data_y[idx]),
761                 y=data_y[idx],
762                 name=data_names[idx],
763                 hoverinfo=u"y+name"
764             )
765         )
766
767     try:
768         # Create plot
769         layout = deepcopy(plot[u"layout"])
770         if layout.get(u"title", None):
771             layout[u"title"] = f"<b>Throughput:</b> {layout[u'title']}"
772         if data_y_max:
773             layout[u"yaxis"][u"range"] = [0, max(data_y_max) + 1]
774         plpl = plgo.Figure(data=traces, layout=layout)
775
776         # Export Plot
777         logging.info(f"    Writing file {plot[u'output-file']}.html.")
778         ploff.plot(
779             plpl,
780             show_link=False,
781             auto_open=False,
782             filename=f"{plot[u'output-file']}.html"
783         )
784     except PlotlyError as err:
785         logging.error(
786             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
787         )
788         return
789
790
791 def plot_mrr_error_bars_name(plot, input_data):
792     """Generate the plot(s) with algorithm: plot_mrr_error_bars_name
793     specified in the specification file.
794
795     :param plot: Plot to generate.
796     :param input_data: Data to process.
797     :type plot: pandas.Series
798     :type input_data: InputData
799     """
800
801     # Transform the data
802     logging.info(
803         f"    Creating data set for the {plot.get(u'type', u'')} "
804         f"{plot.get(u'title', u'')}."
805     )
806     data = input_data.filter_tests_by_name(
807         plot,
808         params=[u"result", u"parent", u"tags", u"type"])
809     if data is None:
810         logging.error(u"No data.")
811         return
812
813     # Prepare the data for the plot
814     data_x = list()
815     data_names = list()
816     data_y_avg = list()
817     data_y_stdev = list()
818     data_y_max = 0
819     hover_info = list()
820     idx = 1
821     for item in plot.get(u"include", tuple()):
822         reg_ex = re.compile(str(item).lower())
823         for job in data:
824             for build in job:
825                 for test_id, test in build.iteritems():
826                     if not re.match(reg_ex, str(test_id).lower()):
827                         continue
828                     try:
829                         data_x.append(idx)
830                         name = re.sub(REGEX_NIC, u'', test[u'parent'].lower().
831                                       replace(u'-mrr', u'').
832                                       replace(u'2n1l-', u''))
833                         data_names.append(f"{idx}. {name}")
834                         data_y_avg.append(
835                             round(test[u"result"][u"receive-rate"], 3)
836                         )
837                         data_y_stdev.append(
838                             round(test[u"result"][u"receive-stdev"], 3)
839                         )
840                         hover_info.append(
841                             f"{data_names[-1]}<br>"
842                             f"average [Gbps]: {data_y_avg[-1]}<br>"
843                             f"stdev [Gbps]: {data_y_stdev[-1]}"
844                         )
845                         if data_y_avg[-1] + data_y_stdev[-1] > data_y_max:
846                             data_y_max = data_y_avg[-1] + data_y_stdev[-1]
847                         idx += 1
848                     except (KeyError, TypeError):
849                         pass
850
851     # Add plot traces
852     traces = list()
853     for idx in range(len(data_x)):
854         traces.append(
855             plgo.Scatter(
856                 x=[data_x[idx], ],
857                 y=[data_y_avg[idx], ],
858                 error_y=dict(
859                     type=u"data",
860                     array=[data_y_stdev[idx], ],
861                     visible=True
862                 ),
863                 name=data_names[idx],
864                 mode=u"markers",
865                 text=hover_info[idx],
866                 hoverinfo=u"text"
867             )
868         )
869
870     try:
871         # Create plot
872         layout = deepcopy(plot[u"layout"])
873         if layout.get(u"title", None):
874             layout[u"title"] = f"<b>Throughput:</b> {layout[u'title']}"
875         if data_y_max:
876             layout[u"yaxis"][u"range"] = [0, int(data_y_max) + 1]
877         plpl = plgo.Figure(data=traces, layout=layout)
878
879         # Export Plot
880         logging.info(f"    Writing file {plot[u'output-file']}.html.")
881         ploff.plot(
882             plpl,
883             show_link=False,
884             auto_open=False,
885             filename=f"{plot[u'output-file']}.html"
886         )
887     except PlotlyError as err:
888         logging.error(
889             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
890         )
891         return
892
893
894 def plot_tsa_name(plot, input_data):
895     """Generate the plot(s) with algorithm:
896     plot_tsa_name
897     specified in the specification file.
898
899     :param plot: Plot to generate.
900     :param input_data: Data to process.
901     :type plot: pandas.Series
902     :type input_data: InputData
903     """
904
905     # Transform the data
906     plot_title = plot.get(u"title", u"")
907     logging.info(
908         f"    Creating data set for the {plot.get(u'type', u'')} {plot_title}."
909     )
910     data = input_data.filter_tests_by_name(
911         plot,
912         params=[u"throughput", u"gbps", u"parent", u"tags", u"type"]
913     )
914     if data is None:
915         logging.error(u"No data.")
916         return
917
918     plot_title = plot_title.lower()
919
920     if u"-gbps" in plot_title:
921         value = u"gbps"
922         h_unit = u"Gbps"
923         multiplier = 1e6
924     else:
925         value = u"throughput"
926         h_unit = u"Mpps"
927         multiplier = 1.0
928
929     y_vals = OrderedDict()
930     for item in plot.get(u"include", tuple()):
931         reg_ex = re.compile(str(item).lower())
932         for job in data:
933             for build in job:
934                 for test_id, test in build.iteritems():
935                     if re.match(reg_ex, str(test_id).lower()):
936                         if y_vals.get(test[u"parent"], None) is None:
937                             y_vals[test[u"parent"]] = {
938                                 u"1": list(),
939                                 u"2": list(),
940                                 u"4": list()
941                             }
942                         try:
943                             if test[u"type"] not in (u"NDRPDR", u"CPS"):
944                                 continue
945
946                             if u"-pdr" in plot_title:
947                                 ttype = u"PDR"
948                             elif u"-ndr" in plot_title:
949                                 ttype = u"NDR"
950                             else:
951                                 continue
952
953                             if u"1C" in test[u"tags"]:
954                                 y_vals[test[u"parent"]][u"1"].append(
955                                     test[value][ttype][u"LOWER"] * multiplier
956                                 )
957                             elif u"2C" in test[u"tags"]:
958                                 y_vals[test[u"parent"]][u"2"].append(
959                                     test[value][ttype][u"LOWER"] * multiplier
960                                 )
961                             elif u"4C" in test[u"tags"]:
962                                 y_vals[test[u"parent"]][u"4"].append(
963                                     test[value][ttype][u"LOWER"] * multiplier
964                                 )
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) * 1e6)
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 = 0
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] / 1e6
997                 y_val_2 = test_vals[u"2"][0] / 1e6 if test_vals[u"2"][0] \
998                     else None
999                 y_val_4 = test_vals[u"4"][0] / 1e6 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         if u"cx556a" in test_name:
1071             limit = plot[u"limits"][u"pci"][u"pci-g3-x8"]
1072         else:
1073             limit = plot[u"limits"][u"pci"][u"pci-g3-x16"]
1074         if limit > pci_limit:
1075             pci_limit = limit
1076
1077     traces = list()
1078     annotations = list()
1079     x_vals = [1, 2, 4]
1080
1081     # Limits:
1082     if u"-gbps" not in plot_title and u"-cps-" not in plot_title:
1083         nic_limit /= 1e6
1084         lnk_limit /= 1e6
1085         pci_limit /= 1e6
1086         min_limit = min((nic_limit, lnk_limit, pci_limit))
1087         if nic_limit == min_limit:
1088             traces.append(plgo.Scatter(
1089                 x=x_vals,
1090                 y=[nic_limit, ] * len(x_vals),
1091                 name=f"NIC: {nic_limit:.2f}Mpps",
1092                 showlegend=False,
1093                 mode=u"lines",
1094                 line=dict(
1095                     dash=u"dot",
1096                     color=COLORS[-1],
1097                     width=1),
1098                 hoverinfo=u"none"
1099             ))
1100             annotations.append(dict(
1101                 x=1,
1102                 y=nic_limit,
1103                 xref=u"x",
1104                 yref=u"y",
1105                 xanchor=u"left",
1106                 yanchor=u"bottom",
1107                 text=f"NIC: {nic_limit:.2f}Mpps",
1108                 font=dict(
1109                     size=14,
1110                     color=COLORS[-1],
1111                 ),
1112                 align=u"left",
1113                 showarrow=False
1114             ))
1115             y_max.append(nic_limit)
1116         elif lnk_limit == min_limit:
1117             traces.append(plgo.Scatter(
1118                 x=x_vals,
1119                 y=[lnk_limit, ] * len(x_vals),
1120                 name=f"Link: {lnk_limit:.2f}Mpps",
1121                 showlegend=False,
1122                 mode=u"lines",
1123                 line=dict(
1124                     dash=u"dot",
1125                     color=COLORS[-1],
1126                     width=1),
1127                 hoverinfo=u"none"
1128             ))
1129             annotations.append(dict(
1130                 x=1,
1131                 y=lnk_limit,
1132                 xref=u"x",
1133                 yref=u"y",
1134                 xanchor=u"left",
1135                 yanchor=u"bottom",
1136                 text=f"Link: {lnk_limit:.2f}Mpps",
1137                 font=dict(
1138                     size=14,
1139                     color=COLORS[-1],
1140                 ),
1141                 align=u"left",
1142                 showarrow=False
1143             ))
1144             y_max.append(lnk_limit)
1145         elif pci_limit == min_limit:
1146             traces.append(plgo.Scatter(
1147                 x=x_vals,
1148                 y=[pci_limit, ] * len(x_vals),
1149                 name=f"PCIe: {pci_limit:.2f}Mpps",
1150                 showlegend=False,
1151                 mode=u"lines",
1152                 line=dict(
1153                     dash=u"dot",
1154                     color=COLORS[-1],
1155                     width=1),
1156                 hoverinfo=u"none"
1157             ))
1158             annotations.append(dict(
1159                 x=1,
1160                 y=pci_limit,
1161                 xref=u"x",
1162                 yref=u"y",
1163                 xanchor=u"left",
1164                 yanchor=u"bottom",
1165                 text=f"PCIe: {pci_limit:.2f}Mpps",
1166                 font=dict(
1167                     size=14,
1168                     color=COLORS[-1],
1169                 ),
1170                 align=u"left",
1171                 showarrow=False
1172             ))
1173             y_max.append(pci_limit)
1174
1175     # Perfect and measured:
1176     cidx = 0
1177     for name, val in vals.items():
1178         hovertext = list()
1179         try:
1180             for idx in range(len(val[u"val"])):
1181                 htext = ""
1182                 if isinstance(val[u"val"][idx], float):
1183                     htext += (
1184                         f"No. of Runs: {val[u'count'][idx]}<br>"
1185                         f"Mean: {val[u'val'][idx]:.2f}{h_unit}<br>"
1186                     )
1187                 if isinstance(val[u"diff"][idx], float):
1188                     htext += f"Diff: {round(val[u'diff'][idx]):.0f}%<br>"
1189                 if isinstance(val[u"rel"][idx], float):
1190                     htext += f"Speedup: {val[u'rel'][idx]:.2f}"
1191                 hovertext.append(htext)
1192             traces.append(
1193                 plgo.Scatter(
1194                     x=x_vals,
1195                     y=val[u"val"],
1196                     name=name,
1197                     legendgroup=name,
1198                     mode=u"lines+markers",
1199                     line=dict(
1200                         color=COLORS[cidx],
1201                         width=2),
1202                     marker=dict(
1203                         symbol=u"circle",
1204                         size=10
1205                     ),
1206                     text=hovertext,
1207                     hoverinfo=u"text+name"
1208                 )
1209             )
1210             traces.append(
1211                 plgo.Scatter(
1212                     x=x_vals,
1213                     y=val[u"ideal"],
1214                     name=f"{name} perfect",
1215                     legendgroup=name,
1216                     showlegend=False,
1217                     mode=u"lines",
1218                     line=dict(
1219                         color=COLORS[cidx],
1220                         width=2,
1221                         dash=u"dash"),
1222                     text=[f"Perfect: {y:.2f}Mpps" for y in val[u"ideal"]],
1223                     hoverinfo=u"text"
1224                 )
1225             )
1226             cidx += 1
1227         except (IndexError, ValueError, KeyError) as err:
1228             logging.warning(f"No data for {name}\n{repr(err)}")
1229
1230     try:
1231         # Create plot
1232         file_type = plot.get(u"output-file-type", u".html")
1233         logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
1234         layout = deepcopy(plot[u"layout"])
1235         if layout.get(u"title", None):
1236             layout[u"title"] = f"<b>Speedup Multi-core:</b> {layout[u'title']}"
1237         layout[u"yaxis"][u"range"] = [0, int(max(y_max) * 1.1)]
1238         layout[u"annotations"].extend(annotations)
1239         plpl = plgo.Figure(data=traces, layout=layout)
1240
1241         # Export Plot
1242         ploff.plot(
1243             plpl,
1244             show_link=False,
1245             auto_open=False,
1246             filename=f"{plot[u'output-file']}{file_type}"
1247         )
1248     except PlotlyError as err:
1249         logging.error(
1250             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1251         )
1252         return
1253
1254
1255 def plot_http_server_perf_box(plot, input_data):
1256     """Generate the plot(s) with algorithm: plot_http_server_perf_box
1257     specified in the specification file.
1258
1259     :param plot: Plot to generate.
1260     :param input_data: Data to process.
1261     :type plot: pandas.Series
1262     :type input_data: InputData
1263     """
1264
1265     # Transform the data
1266     logging.info(
1267         f"    Creating the data set for the {plot.get(u'type', u'')} "
1268         f"{plot.get(u'title', u'')}."
1269     )
1270     data = input_data.filter_data(plot)
1271     if data is None:
1272         logging.error(u"No data.")
1273         return
1274
1275     # Prepare the data for the plot
1276     y_vals = dict()
1277     for job in data:
1278         for build in job:
1279             for test in build:
1280                 if y_vals.get(test[u"name"], None) is None:
1281                     y_vals[test[u"name"]] = list()
1282                 try:
1283                     y_vals[test[u"name"]].append(test[u"result"])
1284                 except (KeyError, TypeError):
1285                     y_vals[test[u"name"]].append(None)
1286
1287     # Add None to the lists with missing data
1288     max_len = 0
1289     nr_of_samples = list()
1290     for val in y_vals.values():
1291         if len(val) > max_len:
1292             max_len = len(val)
1293         nr_of_samples.append(len(val))
1294     for val in y_vals.values():
1295         if len(val) < max_len:
1296             val.extend([None for _ in range(max_len - len(val))])
1297
1298     # Add plot traces
1299     traces = list()
1300     df_y = pd.DataFrame(y_vals)
1301     df_y.head()
1302     for i, col in enumerate(df_y.columns):
1303         name = \
1304             f"{i + 1}. " \
1305             f"({nr_of_samples[i]:02d} " \
1306             f"run{u's' if nr_of_samples[i] > 1 else u''}) " \
1307             f"{col.lower().replace(u'-ndrpdr', u'')}"
1308         if len(name) > 50:
1309             name_lst = name.split(u'-')
1310             name = u""
1311             split_name = True
1312             for segment in name_lst:
1313                 if (len(name) + len(segment) + 1) > 50 and split_name:
1314                     name += u"<br>    "
1315                     split_name = False
1316                 name += segment + u'-'
1317             name = name[:-1]
1318
1319         traces.append(plgo.Box(x=[str(i + 1) + u'.'] * len(df_y[col]),
1320                                y=df_y[col],
1321                                name=name,
1322                                **plot[u"traces"]))
1323     try:
1324         # Create plot
1325         plpl = plgo.Figure(data=traces, layout=plot[u"layout"])
1326
1327         # Export Plot
1328         logging.info(
1329             f"    Writing file {plot[u'output-file']}"
1330             f"{plot[u'output-file-type']}."
1331         )
1332         ploff.plot(
1333             plpl,
1334             show_link=False,
1335             auto_open=False,
1336             filename=f"{plot[u'output-file']}{plot[u'output-file-type']}"
1337         )
1338     except PlotlyError as err:
1339         logging.error(
1340             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1341         )
1342         return
1343
1344
1345 def plot_nf_heatmap(plot, input_data):
1346     """Generate the plot(s) with algorithm: plot_nf_heatmap
1347     specified in the specification file.
1348
1349     :param plot: Plot to generate.
1350     :param input_data: Data to process.
1351     :type plot: pandas.Series
1352     :type input_data: InputData
1353     """
1354
1355     regex_cn = re.compile(r'^(\d*)R(\d*)C$')
1356     regex_test_name = re.compile(r'^.*-(\d+ch|\d+pl)-'
1357                                  r'(\d+mif|\d+vh)-'
1358                                  r'(\d+vm\d+t|\d+dcr\d+t|\d+dcr\d+c).*$')
1359     vals = dict()
1360
1361     # Transform the data
1362     logging.info(
1363         f"    Creating the data set for the {plot.get(u'type', u'')} "
1364         f"{plot.get(u'title', u'')}."
1365     )
1366     data = input_data.filter_data(plot, continue_on_error=True)
1367     if data is None or data.empty:
1368         logging.error(u"No data.")
1369         return
1370
1371     for job in data:
1372         for build in job:
1373             for test in build:
1374                 for tag in test[u"tags"]:
1375                     groups = re.search(regex_cn, tag)
1376                     if groups:
1377                         chain = str(groups.group(1))
1378                         node = str(groups.group(2))
1379                         break
1380                 else:
1381                     continue
1382                 groups = re.search(regex_test_name, test[u"name"])
1383                 if groups and len(groups.groups()) == 3:
1384                     hover_name = (
1385                         f"{str(groups.group(1))}-"
1386                         f"{str(groups.group(2))}-"
1387                         f"{str(groups.group(3))}"
1388                     )
1389                 else:
1390                     hover_name = u""
1391                 if vals.get(chain, None) is None:
1392                     vals[chain] = dict()
1393                 if vals[chain].get(node, None) is None:
1394                     vals[chain][node] = dict(
1395                         name=hover_name,
1396                         vals=list(),
1397                         nr=None,
1398                         mean=None,
1399                         stdev=None
1400                     )
1401                 try:
1402                     if plot[u"include-tests"] == u"MRR":
1403                         result = test[u"result"][u"receive-rate"]
1404                     elif plot[u"include-tests"] == u"PDR":
1405                         result = test[u"throughput"][u"PDR"][u"LOWER"]
1406                     elif plot[u"include-tests"] == u"NDR":
1407                         result = test[u"throughput"][u"NDR"][u"LOWER"]
1408                     else:
1409                         result = None
1410                 except TypeError:
1411                     result = None
1412
1413                 if result:
1414                     vals[chain][node][u"vals"].append(result)
1415
1416     if not vals:
1417         logging.error(u"No data.")
1418         return
1419
1420     txt_chains = list()
1421     txt_nodes = list()
1422     for key_c in vals:
1423         txt_chains.append(key_c)
1424         for key_n in vals[key_c].keys():
1425             txt_nodes.append(key_n)
1426             if vals[key_c][key_n][u"vals"]:
1427                 vals[key_c][key_n][u"nr"] = len(vals[key_c][key_n][u"vals"])
1428                 vals[key_c][key_n][u"mean"] = \
1429                     round(mean(vals[key_c][key_n][u"vals"]) / 1000000, 1)
1430                 vals[key_c][key_n][u"stdev"] = \
1431                     round(stdev(vals[key_c][key_n][u"vals"]) / 1000000, 1)
1432     txt_nodes = list(set(txt_nodes))
1433
1434     def sort_by_int(value):
1435         """Makes possible to sort a list of strings which represent integers.
1436
1437         :param value: Integer as a string.
1438         :type value: str
1439         :returns: Integer representation of input parameter 'value'.
1440         :rtype: int
1441         """
1442         return int(value)
1443
1444     txt_chains = sorted(txt_chains, key=sort_by_int)
1445     txt_nodes = sorted(txt_nodes, key=sort_by_int)
1446
1447     chains = [i + 1 for i in range(len(txt_chains))]
1448     nodes = [i + 1 for i in range(len(txt_nodes))]
1449
1450     data = [list() for _ in range(len(chains))]
1451     for chain in chains:
1452         for node in nodes:
1453             try:
1454                 val = vals[txt_chains[chain - 1]][txt_nodes[node - 1]][u"mean"]
1455             except (KeyError, IndexError):
1456                 val = None
1457             data[chain - 1].append(val)
1458
1459     # Color scales:
1460     my_green = [[0.0, u"rgb(235, 249, 242)"],
1461                 [1.0, u"rgb(45, 134, 89)"]]
1462
1463     my_blue = [[0.0, u"rgb(236, 242, 248)"],
1464                [1.0, u"rgb(57, 115, 172)"]]
1465
1466     my_grey = [[0.0, u"rgb(230, 230, 230)"],
1467                [1.0, u"rgb(102, 102, 102)"]]
1468
1469     hovertext = list()
1470     annotations = list()
1471
1472     text = (u"Test: {name}<br>"
1473             u"Runs: {nr}<br>"
1474             u"Thput: {val}<br>"
1475             u"StDev: {stdev}")
1476
1477     for chain, _ in enumerate(txt_chains):
1478         hover_line = list()
1479         for node, _ in enumerate(txt_nodes):
1480             if data[chain][node] is not None:
1481                 annotations.append(
1482                     dict(
1483                         x=node+1,
1484                         y=chain+1,
1485                         xref=u"x",
1486                         yref=u"y",
1487                         xanchor=u"center",
1488                         yanchor=u"middle",
1489                         text=str(data[chain][node]),
1490                         font=dict(
1491                             size=14,
1492                         ),
1493                         align=u"center",
1494                         showarrow=False
1495                     )
1496                 )
1497                 hover_line.append(text.format(
1498                     name=vals[txt_chains[chain]][txt_nodes[node]][u"name"],
1499                     nr=vals[txt_chains[chain]][txt_nodes[node]][u"nr"],
1500                     val=data[chain][node],
1501                     stdev=vals[txt_chains[chain]][txt_nodes[node]][u"stdev"]))
1502         hovertext.append(hover_line)
1503
1504     traces = [
1505         plgo.Heatmap(
1506             x=nodes,
1507             y=chains,
1508             z=data,
1509             colorbar=dict(
1510                 title=plot.get(u"z-axis", u""),
1511                 titleside=u"right",
1512                 titlefont=dict(
1513                     size=16
1514                 ),
1515                 tickfont=dict(
1516                     size=16,
1517                 ),
1518                 tickformat=u".1f",
1519                 yanchor=u"bottom",
1520                 y=-0.02,
1521                 len=0.925,
1522             ),
1523             showscale=True,
1524             colorscale=my_green,
1525             text=hovertext,
1526             hoverinfo=u"text"
1527         )
1528     ]
1529
1530     for idx, item in enumerate(txt_nodes):
1531         # X-axis, numbers:
1532         annotations.append(
1533             dict(
1534                 x=idx+1,
1535                 y=0.05,
1536                 xref=u"x",
1537                 yref=u"y",
1538                 xanchor=u"center",
1539                 yanchor=u"top",
1540                 text=item,
1541                 font=dict(
1542                     size=16,
1543                 ),
1544                 align=u"center",
1545                 showarrow=False
1546             )
1547         )
1548     for idx, item in enumerate(txt_chains):
1549         # Y-axis, numbers:
1550         annotations.append(
1551             dict(
1552                 x=0.35,
1553                 y=idx+1,
1554                 xref=u"x",
1555                 yref=u"y",
1556                 xanchor=u"right",
1557                 yanchor=u"middle",
1558                 text=item,
1559                 font=dict(
1560                     size=16,
1561                 ),
1562                 align=u"center",
1563                 showarrow=False
1564             )
1565         )
1566     # X-axis, title:
1567     annotations.append(
1568         dict(
1569             x=0.55,
1570             y=-0.15,
1571             xref=u"paper",
1572             yref=u"y",
1573             xanchor=u"center",
1574             yanchor=u"bottom",
1575             text=plot.get(u"x-axis", u""),
1576             font=dict(
1577                 size=16,
1578             ),
1579             align=u"center",
1580             showarrow=False
1581         )
1582     )
1583     # Y-axis, title:
1584     annotations.append(
1585         dict(
1586             x=-0.1,
1587             y=0.5,
1588             xref=u"x",
1589             yref=u"paper",
1590             xanchor=u"center",
1591             yanchor=u"middle",
1592             text=plot.get(u"y-axis", u""),
1593             font=dict(
1594                 size=16,
1595             ),
1596             align=u"center",
1597             textangle=270,
1598             showarrow=False
1599         )
1600     )
1601     updatemenus = list([
1602         dict(
1603             x=1.0,
1604             y=0.0,
1605             xanchor=u"right",
1606             yanchor=u"bottom",
1607             direction=u"up",
1608             buttons=list([
1609                 dict(
1610                     args=[
1611                         {
1612                             u"colorscale": [my_green, ],
1613                             u"reversescale": False
1614                         }
1615                     ],
1616                     label=u"Green",
1617                     method=u"update"
1618                 ),
1619                 dict(
1620                     args=[
1621                         {
1622                             u"colorscale": [my_blue, ],
1623                             u"reversescale": False
1624                         }
1625                     ],
1626                     label=u"Blue",
1627                     method=u"update"
1628                 ),
1629                 dict(
1630                     args=[
1631                         {
1632                             u"colorscale": [my_grey, ],
1633                             u"reversescale": False
1634                         }
1635                     ],
1636                     label=u"Grey",
1637                     method=u"update"
1638                 )
1639             ])
1640         )
1641     ])
1642
1643     try:
1644         layout = deepcopy(plot[u"layout"])
1645     except KeyError as err:
1646         logging.error(f"Finished with error: No layout defined\n{repr(err)}")
1647         return
1648
1649     layout[u"annotations"] = annotations
1650     layout[u'updatemenus'] = updatemenus
1651
1652     try:
1653         # Create plot
1654         plpl = plgo.Figure(data=traces, layout=layout)
1655
1656         # Export Plot
1657         logging.info(f"    Writing file {plot[u'output-file']}.html")
1658         ploff.plot(
1659             plpl,
1660             show_link=False,
1661             auto_open=False,
1662             filename=f"{plot[u'output-file']}.html"
1663         )
1664     except PlotlyError as err:
1665         logging.error(
1666             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1667         )
1668         return