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