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