88af4a46657830040f2c16a7c01d80930037c027
[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
340             for color, graph in enumerate(graphs):
341                 for idx, direction in enumerate((u"direction1", u"direction2")):
342                     previous_x = 0.0
343                     prev_perc = 0.0
344                     xaxis = list()
345                     yaxis = list()
346                     hovertext = list()
347                     try:
348                         decoded = hdrh.histogram.HdrHistogram.decode(
349                             test[u"latency"][graph][direction][u"hdrh"]
350                         )
351                     except hdrh.codec.HdrLengthException:
352                         logging.warning(
353                             f"No data for direction {(u'W-E', u'E-W')[idx % 2]}"
354                         )
355                         continue
356
357                     for item in decoded.get_recorded_iterator():
358                         # The real value is "percentile".
359                         # For 100%, we cut that down to "x_perc" to avoid
360                         # 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
400             layout[u"title"][u"text"] = f"<b>Latency:</b> {name}"
401             layout[u"xaxis"][u"range"] = [0, 5]
402             # layout[u"xaxis"][u"range"] = [
403             #     0, round(log(100.0 / (100.0 - PERCENTILE_MAX), 10))
404             # ]
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_tsa_name(plot, input_data):
703     """Generate the plot(s) with algorithm:
704     plot_tsa_name
705     specified in the specification file.
706
707     :param plot: Plot to generate.
708     :param input_data: Data to process.
709     :type plot: pandas.Series
710     :type input_data: InputData
711     """
712
713     # Transform the data
714     plot_title = plot.get(u"title", u"")
715     logging.info(
716         f"    Creating data set for the {plot.get(u'type', u'')} {plot_title}."
717     )
718     data = input_data.filter_tests_by_name(
719         plot,
720         params=[u"throughput", u"gbps", u"parent", u"tags", u"type"]
721     )
722     if data is None:
723         logging.error(u"No data.")
724         return
725
726     plot_title = plot_title.lower()
727
728     if u"-gbps" in plot_title:
729         value = u"gbps"
730         h_unit = u"Gbps"
731         multiplier = 1e6
732     else:
733         value = u"throughput"
734         h_unit = u"Mpps"
735         multiplier = 1.0
736
737     y_vals = OrderedDict()
738     for item in plot.get(u"include", tuple()):
739         reg_ex = re.compile(str(item).lower())
740         for job in data:
741             for build in job:
742                 for test_id, test in build.iteritems():
743                     if re.match(reg_ex, str(test_id).lower()):
744                         if y_vals.get(test[u"parent"], None) is None:
745                             y_vals[test[u"parent"]] = {
746                                 u"1": list(),
747                                 u"2": list(),
748                                 u"4": list()
749                             }
750                         try:
751                             if test[u"type"] not in (u"NDRPDR", u"CPS"):
752                                 continue
753
754                             if u"-pdr" in plot_title:
755                                 ttype = u"PDR"
756                             elif u"-ndr" in plot_title:
757                                 ttype = u"NDR"
758                             else:
759                                 continue
760
761                             if u"1C" in test[u"tags"]:
762                                 y_vals[test[u"parent"]][u"1"].append(
763                                     test[value][ttype][u"LOWER"] * multiplier
764                                 )
765                             elif u"2C" in test[u"tags"]:
766                                 y_vals[test[u"parent"]][u"2"].append(
767                                     test[value][ttype][u"LOWER"] * multiplier
768                                 )
769                             elif u"4C" in test[u"tags"]:
770                                 y_vals[test[u"parent"]][u"4"].append(
771                                     test[value][ttype][u"LOWER"] * multiplier
772                                 )
773                         except (KeyError, TypeError):
774                             pass
775
776     if not y_vals:
777         logging.warning(f"No data for the plot {plot.get(u'title', u'')}")
778         return
779
780     y_1c_max = dict()
781     for test_name, test_vals in y_vals.items():
782         for key, test_val in test_vals.items():
783             if test_val:
784                 avg_val = sum(test_val) / len(test_val)
785                 y_vals[test_name][key] = [avg_val, len(test_val)]
786                 ideal = avg_val / (int(key) * 1e6)
787                 if test_name not in y_1c_max or ideal > y_1c_max[test_name]:
788                     y_1c_max[test_name] = ideal
789
790     vals = OrderedDict()
791     y_max = list()
792     nic_limit = 0
793     lnk_limit = 0
794     pci_limit = 0
795     for test_name, test_vals in y_vals.items():
796         try:
797             if test_vals[u"1"][1]:
798                 name = re.sub(
799                     REGEX_NIC,
800                     u"",
801                     test_name.replace(u'-ndrpdr', u'').replace(u'2n1l-', u'')
802                 )
803                 vals[name] = OrderedDict()
804                 y_val_1 = test_vals[u"1"][0] / 1e6
805                 y_val_2 = test_vals[u"2"][0] / 1e6 if test_vals[u"2"][0] \
806                     else None
807                 y_val_4 = test_vals[u"4"][0] / 1e6 if test_vals[u"4"][0] \
808                     else None
809
810                 vals[name][u"val"] = [y_val_1, y_val_2, y_val_4]
811                 vals[name][u"rel"] = [1.0, None, None]
812                 vals[name][u"ideal"] = [
813                     y_1c_max[test_name],
814                     y_1c_max[test_name] * 2,
815                     y_1c_max[test_name] * 4
816                 ]
817                 vals[name][u"diff"] = [
818                     (y_val_1 - y_1c_max[test_name]) * 100 / y_val_1, None, None
819                 ]
820                 vals[name][u"count"] = [
821                     test_vals[u"1"][1],
822                     test_vals[u"2"][1],
823                     test_vals[u"4"][1]
824                 ]
825
826                 try:
827                     val_max = max(vals[name][u"val"])
828                 except ValueError as err:
829                     logging.error(repr(err))
830                     continue
831                 if val_max:
832                     y_max.append(val_max)
833
834                 if y_val_2:
835                     vals[name][u"rel"][1] = round(y_val_2 / y_val_1, 2)
836                     vals[name][u"diff"][1] = \
837                         (y_val_2 - vals[name][u"ideal"][1]) * 100 / y_val_2
838                 if y_val_4:
839                     vals[name][u"rel"][2] = round(y_val_4 / y_val_1, 2)
840                     vals[name][u"diff"][2] = \
841                         (y_val_4 - vals[name][u"ideal"][2]) * 100 / y_val_4
842         except IndexError as err:
843             logging.warning(f"No data for {test_name}")
844             logging.warning(repr(err))
845
846         # Limits:
847         if u"x520" in test_name:
848             limit = plot[u"limits"][u"nic"][u"x520"]
849         elif u"x710" in test_name:
850             limit = plot[u"limits"][u"nic"][u"x710"]
851         elif u"xxv710" in test_name:
852             limit = plot[u"limits"][u"nic"][u"xxv710"]
853         elif u"xl710" in test_name:
854             limit = plot[u"limits"][u"nic"][u"xl710"]
855         elif u"x553" in test_name:
856             limit = plot[u"limits"][u"nic"][u"x553"]
857         elif u"cx556a" in test_name:
858             limit = plot[u"limits"][u"nic"][u"cx556a"]
859         else:
860             limit = 0
861         if limit > nic_limit:
862             nic_limit = limit
863
864         mul = 2 if u"ge2p" in test_name else 1
865         if u"10ge" in test_name:
866             limit = plot[u"limits"][u"link"][u"10ge"] * mul
867         elif u"25ge" in test_name:
868             limit = plot[u"limits"][u"link"][u"25ge"] * mul
869         elif u"40ge" in test_name:
870             limit = plot[u"limits"][u"link"][u"40ge"] * mul
871         elif u"100ge" in test_name:
872             limit = plot[u"limits"][u"link"][u"100ge"] * mul
873         else:
874             limit = 0
875         if limit > lnk_limit:
876             lnk_limit = limit
877
878         if u"cx556a" in test_name:
879             limit = plot[u"limits"][u"pci"][u"pci-g3-x8"]
880         else:
881             limit = plot[u"limits"][u"pci"][u"pci-g3-x16"]
882         if limit > pci_limit:
883             pci_limit = limit
884
885     traces = list()
886     annotations = list()
887     x_vals = [1, 2, 4]
888
889     # Limits:
890     if u"-gbps" not in plot_title and u"-cps-" not in plot_title:
891         nic_limit /= 1e6
892         lnk_limit /= 1e6
893         pci_limit /= 1e6
894         min_limit = min((nic_limit, lnk_limit, pci_limit))
895         if nic_limit == min_limit:
896             traces.append(plgo.Scatter(
897                 x=x_vals,
898                 y=[nic_limit, ] * len(x_vals),
899                 name=f"NIC: {nic_limit:.2f}Mpps",
900                 showlegend=False,
901                 mode=u"lines",
902                 line=dict(
903                     dash=u"dot",
904                     color=COLORS[-1],
905                     width=1),
906                 hoverinfo=u"none"
907             ))
908             annotations.append(dict(
909                 x=1,
910                 y=nic_limit,
911                 xref=u"x",
912                 yref=u"y",
913                 xanchor=u"left",
914                 yanchor=u"bottom",
915                 text=f"NIC: {nic_limit:.2f}Mpps",
916                 font=dict(
917                     size=14,
918                     color=COLORS[-1],
919                 ),
920                 align=u"left",
921                 showarrow=False
922             ))
923             y_max.append(nic_limit)
924         elif lnk_limit == min_limit:
925             traces.append(plgo.Scatter(
926                 x=x_vals,
927                 y=[lnk_limit, ] * len(x_vals),
928                 name=f"Link: {lnk_limit:.2f}Mpps",
929                 showlegend=False,
930                 mode=u"lines",
931                 line=dict(
932                     dash=u"dot",
933                     color=COLORS[-1],
934                     width=1),
935                 hoverinfo=u"none"
936             ))
937             annotations.append(dict(
938                 x=1,
939                 y=lnk_limit,
940                 xref=u"x",
941                 yref=u"y",
942                 xanchor=u"left",
943                 yanchor=u"bottom",
944                 text=f"Link: {lnk_limit:.2f}Mpps",
945                 font=dict(
946                     size=14,
947                     color=COLORS[-1],
948                 ),
949                 align=u"left",
950                 showarrow=False
951             ))
952             y_max.append(lnk_limit)
953         elif pci_limit == min_limit:
954             traces.append(plgo.Scatter(
955                 x=x_vals,
956                 y=[pci_limit, ] * len(x_vals),
957                 name=f"PCIe: {pci_limit:.2f}Mpps",
958                 showlegend=False,
959                 mode=u"lines",
960                 line=dict(
961                     dash=u"dot",
962                     color=COLORS[-1],
963                     width=1),
964                 hoverinfo=u"none"
965             ))
966             annotations.append(dict(
967                 x=1,
968                 y=pci_limit,
969                 xref=u"x",
970                 yref=u"y",
971                 xanchor=u"left",
972                 yanchor=u"bottom",
973                 text=f"PCIe: {pci_limit:.2f}Mpps",
974                 font=dict(
975                     size=14,
976                     color=COLORS[-1],
977                 ),
978                 align=u"left",
979                 showarrow=False
980             ))
981             y_max.append(pci_limit)
982
983     # Perfect and measured:
984     cidx = 0
985     for name, val in vals.items():
986         hovertext = list()
987         try:
988             for idx in range(len(val[u"val"])):
989                 htext = ""
990                 if isinstance(val[u"val"][idx], float):
991                     htext += (
992                         f"No. of Runs: {val[u'count'][idx]}<br>"
993                         f"Mean: {val[u'val'][idx]:.2f}{h_unit}<br>"
994                     )
995                 if isinstance(val[u"diff"][idx], float):
996                     htext += f"Diff: {round(val[u'diff'][idx]):.0f}%<br>"
997                 if isinstance(val[u"rel"][idx], float):
998                     htext += f"Speedup: {val[u'rel'][idx]:.2f}"
999                 hovertext.append(htext)
1000             traces.append(
1001                 plgo.Scatter(
1002                     x=x_vals,
1003                     y=val[u"val"],
1004                     name=name,
1005                     legendgroup=name,
1006                     mode=u"lines+markers",
1007                     line=dict(
1008                         color=COLORS[cidx],
1009                         width=2),
1010                     marker=dict(
1011                         symbol=u"circle",
1012                         size=10
1013                     ),
1014                     text=hovertext,
1015                     hoverinfo=u"text+name"
1016                 )
1017             )
1018             traces.append(
1019                 plgo.Scatter(
1020                     x=x_vals,
1021                     y=val[u"ideal"],
1022                     name=f"{name} perfect",
1023                     legendgroup=name,
1024                     showlegend=False,
1025                     mode=u"lines",
1026                     line=dict(
1027                         color=COLORS[cidx],
1028                         width=2,
1029                         dash=u"dash"),
1030                     text=[f"Perfect: {y:.2f}Mpps" for y in val[u"ideal"]],
1031                     hoverinfo=u"text"
1032                 )
1033             )
1034             cidx += 1
1035         except (IndexError, ValueError, KeyError) as err:
1036             logging.warning(f"No data for {name}\n{repr(err)}")
1037
1038     try:
1039         # Create plot
1040         file_type = plot.get(u"output-file-type", u".html")
1041         logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
1042         layout = deepcopy(plot[u"layout"])
1043         if layout.get(u"title", None):
1044             layout[u"title"] = f"<b>Speedup Multi-core:</b> {layout[u'title']}"
1045         layout[u"yaxis"][u"range"] = [0, int(max(y_max) * 1.1)]
1046         layout[u"annotations"].extend(annotations)
1047         plpl = plgo.Figure(data=traces, layout=layout)
1048
1049         # Export Plot
1050         ploff.plot(
1051             plpl,
1052             show_link=False,
1053             auto_open=False,
1054             filename=f"{plot[u'output-file']}{file_type}"
1055         )
1056     except PlotlyError as err:
1057         logging.error(
1058             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1059         )
1060         return
1061
1062
1063 def plot_http_server_perf_box(plot, input_data):
1064     """Generate the plot(s) with algorithm: plot_http_server_perf_box
1065     specified in the specification file.
1066
1067     :param plot: Plot to generate.
1068     :param input_data: Data to process.
1069     :type plot: pandas.Series
1070     :type input_data: InputData
1071     """
1072
1073     # Transform the data
1074     logging.info(
1075         f"    Creating the data set for the {plot.get(u'type', u'')} "
1076         f"{plot.get(u'title', u'')}."
1077     )
1078     data = input_data.filter_data(plot)
1079     if data is None:
1080         logging.error(u"No data.")
1081         return
1082
1083     # Prepare the data for the plot
1084     y_vals = dict()
1085     for job in data:
1086         for build in job:
1087             for test in build:
1088                 if y_vals.get(test[u"name"], None) is None:
1089                     y_vals[test[u"name"]] = list()
1090                 try:
1091                     y_vals[test[u"name"]].append(test[u"result"])
1092                 except (KeyError, TypeError):
1093                     y_vals[test[u"name"]].append(None)
1094
1095     # Add None to the lists with missing data
1096     max_len = 0
1097     nr_of_samples = list()
1098     for val in y_vals.values():
1099         if len(val) > max_len:
1100             max_len = len(val)
1101         nr_of_samples.append(len(val))
1102     for val in y_vals.values():
1103         if len(val) < max_len:
1104             val.extend([None for _ in range(max_len - len(val))])
1105
1106     # Add plot traces
1107     traces = list()
1108     df_y = pd.DataFrame(y_vals)
1109     df_y.head()
1110     for i, col in enumerate(df_y.columns):
1111         name = \
1112             f"{i + 1}. " \
1113             f"({nr_of_samples[i]:02d} " \
1114             f"run{u's' if nr_of_samples[i] > 1 else u''}) " \
1115             f"{col.lower().replace(u'-ndrpdr', u'')}"
1116         if len(name) > 50:
1117             name_lst = name.split(u'-')
1118             name = u""
1119             split_name = True
1120             for segment in name_lst:
1121                 if (len(name) + len(segment) + 1) > 50 and split_name:
1122                     name += u"<br>    "
1123                     split_name = False
1124                 name += segment + u'-'
1125             name = name[:-1]
1126
1127         traces.append(plgo.Box(x=[str(i + 1) + u'.'] * len(df_y[col]),
1128                                y=df_y[col],
1129                                name=name,
1130                                **plot[u"traces"]))
1131     try:
1132         # Create plot
1133         plpl = plgo.Figure(data=traces, layout=plot[u"layout"])
1134
1135         # Export Plot
1136         logging.info(
1137             f"    Writing file {plot[u'output-file']}"
1138             f"{plot[u'output-file-type']}."
1139         )
1140         ploff.plot(
1141             plpl,
1142             show_link=False,
1143             auto_open=False,
1144             filename=f"{plot[u'output-file']}{plot[u'output-file-type']}"
1145         )
1146     except PlotlyError as err:
1147         logging.error(
1148             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1149         )
1150         return
1151
1152
1153 def plot_nf_heatmap(plot, input_data):
1154     """Generate the plot(s) with algorithm: plot_nf_heatmap
1155     specified in the specification file.
1156
1157     :param plot: Plot to generate.
1158     :param input_data: Data to process.
1159     :type plot: pandas.Series
1160     :type input_data: InputData
1161     """
1162
1163     regex_cn = re.compile(r'^(\d*)R(\d*)C$')
1164     regex_test_name = re.compile(r'^.*-(\d+ch|\d+pl)-'
1165                                  r'(\d+mif|\d+vh)-'
1166                                  r'(\d+vm\d+t|\d+dcr\d+t|\d+dcr\d+c).*$')
1167     vals = dict()
1168
1169     # Transform the data
1170     logging.info(
1171         f"    Creating the data set for the {plot.get(u'type', u'')} "
1172         f"{plot.get(u'title', u'')}."
1173     )
1174     data = input_data.filter_data(plot, continue_on_error=True)
1175     if data is None or data.empty:
1176         logging.error(u"No data.")
1177         return
1178
1179     for job in data:
1180         for build in job:
1181             for test in build:
1182                 for tag in test[u"tags"]:
1183                     groups = re.search(regex_cn, tag)
1184                     if groups:
1185                         chain = str(groups.group(1))
1186                         node = str(groups.group(2))
1187                         break
1188                 else:
1189                     continue
1190                 groups = re.search(regex_test_name, test[u"name"])
1191                 if groups and len(groups.groups()) == 3:
1192                     hover_name = (
1193                         f"{str(groups.group(1))}-"
1194                         f"{str(groups.group(2))}-"
1195                         f"{str(groups.group(3))}"
1196                     )
1197                 else:
1198                     hover_name = u""
1199                 if vals.get(chain, None) is None:
1200                     vals[chain] = dict()
1201                 if vals[chain].get(node, None) is None:
1202                     vals[chain][node] = dict(
1203                         name=hover_name,
1204                         vals=list(),
1205                         nr=None,
1206                         mean=None,
1207                         stdev=None
1208                     )
1209                 try:
1210                     if plot[u"include-tests"] == u"MRR":
1211                         result = test[u"result"][u"receive-rate"]
1212                     elif plot[u"include-tests"] == u"PDR":
1213                         result = test[u"throughput"][u"PDR"][u"LOWER"]
1214                     elif plot[u"include-tests"] == u"NDR":
1215                         result = test[u"throughput"][u"NDR"][u"LOWER"]
1216                     else:
1217                         result = None
1218                 except TypeError:
1219                     result = None
1220
1221                 if result:
1222                     vals[chain][node][u"vals"].append(result)
1223
1224     if not vals:
1225         logging.error(u"No data.")
1226         return
1227
1228     txt_chains = list()
1229     txt_nodes = list()
1230     for key_c in vals:
1231         txt_chains.append(key_c)
1232         for key_n in vals[key_c].keys():
1233             txt_nodes.append(key_n)
1234             if vals[key_c][key_n][u"vals"]:
1235                 vals[key_c][key_n][u"nr"] = len(vals[key_c][key_n][u"vals"])
1236                 vals[key_c][key_n][u"mean"] = \
1237                     round(mean(vals[key_c][key_n][u"vals"]) / 1000000, 1)
1238                 vals[key_c][key_n][u"stdev"] = \
1239                     round(stdev(vals[key_c][key_n][u"vals"]) / 1000000, 1)
1240     txt_nodes = list(set(txt_nodes))
1241
1242     def sort_by_int(value):
1243         """Makes possible to sort a list of strings which represent integers.
1244
1245         :param value: Integer as a string.
1246         :type value: str
1247         :returns: Integer representation of input parameter 'value'.
1248         :rtype: int
1249         """
1250         return int(value)
1251
1252     txt_chains = sorted(txt_chains, key=sort_by_int)
1253     txt_nodes = sorted(txt_nodes, key=sort_by_int)
1254
1255     chains = [i + 1 for i in range(len(txt_chains))]
1256     nodes = [i + 1 for i in range(len(txt_nodes))]
1257
1258     data = [list() for _ in range(len(chains))]
1259     for chain in chains:
1260         for node in nodes:
1261             try:
1262                 val = vals[txt_chains[chain - 1]][txt_nodes[node - 1]][u"mean"]
1263             except (KeyError, IndexError):
1264                 val = None
1265             data[chain - 1].append(val)
1266
1267     # Color scales:
1268     my_green = [[0.0, u"rgb(235, 249, 242)"],
1269                 [1.0, u"rgb(45, 134, 89)"]]
1270
1271     my_blue = [[0.0, u"rgb(236, 242, 248)"],
1272                [1.0, u"rgb(57, 115, 172)"]]
1273
1274     my_grey = [[0.0, u"rgb(230, 230, 230)"],
1275                [1.0, u"rgb(102, 102, 102)"]]
1276
1277     hovertext = list()
1278     annotations = list()
1279
1280     text = (u"Test: {name}<br>"
1281             u"Runs: {nr}<br>"
1282             u"Thput: {val}<br>"
1283             u"StDev: {stdev}")
1284
1285     for chain, _ in enumerate(txt_chains):
1286         hover_line = list()
1287         for node, _ in enumerate(txt_nodes):
1288             if data[chain][node] is not None:
1289                 annotations.append(
1290                     dict(
1291                         x=node+1,
1292                         y=chain+1,
1293                         xref=u"x",
1294                         yref=u"y",
1295                         xanchor=u"center",
1296                         yanchor=u"middle",
1297                         text=str(data[chain][node]),
1298                         font=dict(
1299                             size=14,
1300                         ),
1301                         align=u"center",
1302                         showarrow=False
1303                     )
1304                 )
1305                 hover_line.append(text.format(
1306                     name=vals[txt_chains[chain]][txt_nodes[node]][u"name"],
1307                     nr=vals[txt_chains[chain]][txt_nodes[node]][u"nr"],
1308                     val=data[chain][node],
1309                     stdev=vals[txt_chains[chain]][txt_nodes[node]][u"stdev"]))
1310         hovertext.append(hover_line)
1311
1312     traces = [
1313         plgo.Heatmap(
1314             x=nodes,
1315             y=chains,
1316             z=data,
1317             colorbar=dict(
1318                 title=plot.get(u"z-axis", u""),
1319                 titleside=u"right",
1320                 titlefont=dict(
1321                     size=16
1322                 ),
1323                 tickfont=dict(
1324                     size=16,
1325                 ),
1326                 tickformat=u".1f",
1327                 yanchor=u"bottom",
1328                 y=-0.02,
1329                 len=0.925,
1330             ),
1331             showscale=True,
1332             colorscale=my_green,
1333             text=hovertext,
1334             hoverinfo=u"text"
1335         )
1336     ]
1337
1338     for idx, item in enumerate(txt_nodes):
1339         # X-axis, numbers:
1340         annotations.append(
1341             dict(
1342                 x=idx+1,
1343                 y=0.05,
1344                 xref=u"x",
1345                 yref=u"y",
1346                 xanchor=u"center",
1347                 yanchor=u"top",
1348                 text=item,
1349                 font=dict(
1350                     size=16,
1351                 ),
1352                 align=u"center",
1353                 showarrow=False
1354             )
1355         )
1356     for idx, item in enumerate(txt_chains):
1357         # Y-axis, numbers:
1358         annotations.append(
1359             dict(
1360                 x=0.35,
1361                 y=idx+1,
1362                 xref=u"x",
1363                 yref=u"y",
1364                 xanchor=u"right",
1365                 yanchor=u"middle",
1366                 text=item,
1367                 font=dict(
1368                     size=16,
1369                 ),
1370                 align=u"center",
1371                 showarrow=False
1372             )
1373         )
1374     # X-axis, title:
1375     annotations.append(
1376         dict(
1377             x=0.55,
1378             y=-0.15,
1379             xref=u"paper",
1380             yref=u"y",
1381             xanchor=u"center",
1382             yanchor=u"bottom",
1383             text=plot.get(u"x-axis", u""),
1384             font=dict(
1385                 size=16,
1386             ),
1387             align=u"center",
1388             showarrow=False
1389         )
1390     )
1391     # Y-axis, title:
1392     annotations.append(
1393         dict(
1394             x=-0.1,
1395             y=0.5,
1396             xref=u"x",
1397             yref=u"paper",
1398             xanchor=u"center",
1399             yanchor=u"middle",
1400             text=plot.get(u"y-axis", u""),
1401             font=dict(
1402                 size=16,
1403             ),
1404             align=u"center",
1405             textangle=270,
1406             showarrow=False
1407         )
1408     )
1409     updatemenus = list([
1410         dict(
1411             x=1.0,
1412             y=0.0,
1413             xanchor=u"right",
1414             yanchor=u"bottom",
1415             direction=u"up",
1416             buttons=list([
1417                 dict(
1418                     args=[
1419                         {
1420                             u"colorscale": [my_green, ],
1421                             u"reversescale": False
1422                         }
1423                     ],
1424                     label=u"Green",
1425                     method=u"update"
1426                 ),
1427                 dict(
1428                     args=[
1429                         {
1430                             u"colorscale": [my_blue, ],
1431                             u"reversescale": False
1432                         }
1433                     ],
1434                     label=u"Blue",
1435                     method=u"update"
1436                 ),
1437                 dict(
1438                     args=[
1439                         {
1440                             u"colorscale": [my_grey, ],
1441                             u"reversescale": False
1442                         }
1443                     ],
1444                     label=u"Grey",
1445                     method=u"update"
1446                 )
1447             ])
1448         )
1449     ])
1450
1451     try:
1452         layout = deepcopy(plot[u"layout"])
1453     except KeyError as err:
1454         logging.error(f"Finished with error: No layout defined\n{repr(err)}")
1455         return
1456
1457     layout[u"annotations"] = annotations
1458     layout[u'updatemenus'] = updatemenus
1459
1460     try:
1461         # Create plot
1462         plpl = plgo.Figure(data=traces, layout=layout)
1463
1464         # Export Plot
1465         logging.info(f"    Writing file {plot[u'output-file']}.html")
1466         ploff.plot(
1467             plpl,
1468             show_link=False,
1469             auto_open=False,
1470             filename=f"{plot[u'output-file']}.html"
1471         )
1472     except PlotlyError as err:
1473         logging.error(
1474             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1475         )
1476         return