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