c9018f4429a293d57b220214e6fc6b4a29d85bcd
[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
492         tst_name = re.sub(REGEX_NIC, u"",
493                           col.lower().replace(u'-reconf', u'').
494                           replace(u'2n1l-', u'').replace(u'2n-', u'').
495                           replace(u'-testpmd', u''))
496
497         traces.append(plgo.Box(
498             x=[str(i + 1) + u'.'] * len(df_y[col]),
499             y=df_y[col],
500             name=(
501                 f"{i + 1}. "
502                 f"({nr_of_samples[i]:02d} "
503                 f"run{u's' if nr_of_samples[i] > 1 else u''}, "
504                 f"packets lost average: {mean(loss[col]):.1f}) "
505                 f"{u'-'.join(tst_name.split(u'-')[2:])}"
506             ),
507             hoverinfo=u"y+name"
508         ))
509     try:
510         # Create plot
511         layout = deepcopy(plot[u"layout"])
512         layout[u"title"] = f"<b>Time Lost:</b> {layout[u'title']}"
513         layout[u"yaxis"][u"title"] = u"<b>Effective Blocked Time [s]</b>"
514         layout[u"legend"][u"font"][u"size"] = 14
515         layout[u"yaxis"].pop(u"range")
516         plpl = plgo.Figure(data=traces, layout=layout)
517
518         # Export Plot
519         file_type = plot.get(u"output-file-type", u".html")
520         logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
521         ploff.plot(
522             plpl,
523             show_link=False,
524             auto_open=False,
525             filename=f"{plot[u'output-file']}{file_type}"
526         )
527     except PlotlyError as err:
528         logging.error(
529             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
530         )
531         return
532
533
534 def plot_perf_box_name(plot, input_data):
535     """Generate the plot(s) with algorithm: plot_perf_box_name
536     specified in the specification file.
537
538     :param plot: Plot to generate.
539     :param input_data: Data to process.
540     :type plot: pandas.Series
541     :type input_data: InputData
542     """
543
544     # Transform the data
545     logging.info(
546         f"    Creating data set for the {plot.get(u'type', u'')} "
547         f"{plot.get(u'title', u'')}."
548     )
549     data = input_data.filter_tests_by_name(
550         plot,
551         params=[u"throughput", u"gbps", u"result", u"parent", u"tags", u"type"])
552     if data is None:
553         logging.error(u"No data.")
554         return
555
556     # Prepare the data for the plot
557     plot_title = plot.get(u"title", u"").lower()
558
559     if u"-gbps" in plot_title:
560         value = u"gbps"
561         multiplier = 1e6
562     else:
563         value = u"throughput"
564         multiplier = 1.0
565     y_vals = OrderedDict()
566     test_type = u""
567
568     for item in plot.get(u"include", tuple()):
569         reg_ex = re.compile(str(item).lower())
570         for job in data:
571             for build in job:
572                 for test_id, test in build.iteritems():
573                     if not re.match(reg_ex, str(test_id).lower()):
574                         continue
575                     if y_vals.get(test[u"parent"], None) is None:
576                         y_vals[test[u"parent"]] = list()
577                     try:
578                         if test[u"type"] in (u"NDRPDR", u"CPS"):
579                             test_type = test[u"type"]
580
581                             if u"-pdr" in plot_title:
582                                 ttype = u"PDR"
583                             elif u"-ndr" in plot_title:
584                                 ttype = u"NDR"
585                             else:
586                                 raise RuntimeError(
587                                     u"Wrong title. No information about test "
588                                     u"type. Add '-ndr' or '-pdr' to the test "
589                                     u"title."
590                                 )
591
592                             y_vals[test[u"parent"]].append(
593                                 test[value][ttype][u"LOWER"] * multiplier
594                             )
595
596                         elif test[u"type"] in (u"SOAK",):
597                             y_vals[test[u"parent"]]. \
598                                 append(test[u"throughput"][u"LOWER"])
599                             test_type = u"SOAK"
600
601                         elif test[u"type"] in (u"HOSTSTACK",):
602                             if u"LDPRELOAD" in test[u"tags"]:
603                                 y_vals[test[u"parent"]].append(
604                                     float(
605                                         test[u"result"][u"bits_per_second"]
606                                     ) / 1e3
607                                 )
608                             elif u"VPPECHO" in test[u"tags"]:
609                                 y_vals[test[u"parent"]].append(
610                                     (float(
611                                         test[u"result"][u"client"][u"tx_data"]
612                                     ) * 8 / 1e3) /
613                                     ((float(
614                                         test[u"result"][u"client"][u"time"]
615                                     ) +
616                                       float(
617                                           test[u"result"][u"server"][u"time"])
618                                       ) / 2)
619                                 )
620                             test_type = u"HOSTSTACK"
621
622                         else:
623                             continue
624
625                     except (KeyError, TypeError):
626                         y_vals[test[u"parent"]].append(None)
627
628     # Add None to the lists with missing data
629     max_len = 0
630     nr_of_samples = list()
631     for val in y_vals.values():
632         if len(val) > max_len:
633             max_len = len(val)
634         nr_of_samples.append(len(val))
635     for val in y_vals.values():
636         if len(val) < max_len:
637             val.extend([None for _ in range(max_len - len(val))])
638
639     # Add plot traces
640     traces = list()
641     df_y = pd.DataFrame(y_vals)
642     df_y.head()
643     y_max = list()
644     for i, col in enumerate(df_y.columns):
645         tst_name = re.sub(REGEX_NIC, u"",
646                           col.lower().replace(u'-ndrpdr', u'').
647                           replace(u'2n1l-', u''))
648         kwargs = dict(
649             x=[str(i + 1) + u'.'] * len(df_y[col]),
650             y=[y / 1e6 if y else None for y in df_y[col]],
651             name=(
652                 f"{i + 1}. "
653                 f"({nr_of_samples[i]:02d} "
654                 f"run{u's' if nr_of_samples[i] > 1 else u''}) "
655                 f"{tst_name}"
656             ),
657             hoverinfo=u"y+name"
658         )
659         if test_type in (u"SOAK", ):
660             kwargs[u"boxpoints"] = u"all"
661
662         traces.append(plgo.Box(**kwargs))
663
664         try:
665             val_max = max(df_y[col])
666             if val_max:
667                 y_max.append(int(val_max / 1e6) + 2)
668         except (ValueError, TypeError) as err:
669             logging.error(repr(err))
670             continue
671
672     try:
673         # Create plot
674         layout = deepcopy(plot[u"layout"])
675         if layout.get(u"title", None):
676             if test_type in (u"HOSTSTACK", ):
677                 layout[u"title"] = f"<b>Bandwidth:</b> {layout[u'title']}"
678             elif test_type in (u"CPS", ):
679                 layout[u"title"] = f"<b>CPS:</b> {layout[u'title']}"
680             else:
681                 layout[u"title"] = f"<b>Throughput:</b> {layout[u'title']}"
682         if y_max:
683             layout[u"yaxis"][u"range"] = [0, max(y_max)]
684         plpl = plgo.Figure(data=traces, layout=layout)
685
686         # Export Plot
687         logging.info(f"    Writing file {plot[u'output-file']}.html.")
688         ploff.plot(
689             plpl,
690             show_link=False,
691             auto_open=False,
692             filename=f"{plot[u'output-file']}.html"
693         )
694     except PlotlyError as err:
695         logging.error(
696             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
697         )
698         return
699
700
701 def plot_tsa_name(plot, input_data):
702     """Generate the plot(s) with algorithm:
703     plot_tsa_name
704     specified in the specification file.
705
706     :param plot: Plot to generate.
707     :param input_data: Data to process.
708     :type plot: pandas.Series
709     :type input_data: InputData
710     """
711
712     # Transform the data
713     plot_title = plot.get(u"title", u"")
714     logging.info(
715         f"    Creating data set for the {plot.get(u'type', u'')} {plot_title}."
716     )
717     data = input_data.filter_tests_by_name(
718         plot,
719         params=[u"throughput", u"gbps", u"parent", u"tags", u"type"]
720     )
721     if data is None:
722         logging.error(u"No data.")
723         return
724
725     plot_title = plot_title.lower()
726
727     if u"-gbps" in plot_title:
728         value = u"gbps"
729         h_unit = u"Gbps"
730         multiplier = 1e6
731     else:
732         value = u"throughput"
733         h_unit = u"Mpps"
734         multiplier = 1.0
735
736     y_vals = OrderedDict()
737     for item in plot.get(u"include", tuple()):
738         reg_ex = re.compile(str(item).lower())
739         for job in data:
740             for build in job:
741                 for test_id, test in build.iteritems():
742                     if re.match(reg_ex, str(test_id).lower()):
743                         if y_vals.get(test[u"parent"], None) is None:
744                             y_vals[test[u"parent"]] = {
745                                 u"1": list(),
746                                 u"2": list(),
747                                 u"4": list()
748                             }
749                         try:
750                             if test[u"type"] not in (u"NDRPDR", u"CPS"):
751                                 continue
752
753                             if u"-pdr" in plot_title:
754                                 ttype = u"PDR"
755                             elif u"-ndr" in plot_title:
756                                 ttype = u"NDR"
757                             else:
758                                 continue
759
760                             if u"1C" in test[u"tags"]:
761                                 y_vals[test[u"parent"]][u"1"].append(
762                                     test[value][ttype][u"LOWER"] * multiplier
763                                 )
764                             elif u"2C" in test[u"tags"]:
765                                 y_vals[test[u"parent"]][u"2"].append(
766                                     test[value][ttype][u"LOWER"] * multiplier
767                                 )
768                             elif u"4C" in test[u"tags"]:
769                                 y_vals[test[u"parent"]][u"4"].append(
770                                     test[value][ttype][u"LOWER"] * multiplier
771                                 )
772                         except (KeyError, TypeError):
773                             pass
774
775     if not y_vals:
776         logging.warning(f"No data for the plot {plot.get(u'title', u'')}")
777         return
778
779     y_1c_max = dict()
780     for test_name, test_vals in y_vals.items():
781         for key, test_val in test_vals.items():
782             if test_val:
783                 avg_val = sum(test_val) / len(test_val)
784                 y_vals[test_name][key] = [avg_val, len(test_val)]
785                 ideal = avg_val / (int(key) * 1e6)
786                 if test_name not in y_1c_max or ideal > y_1c_max[test_name]:
787                     y_1c_max[test_name] = ideal
788
789     vals = OrderedDict()
790     y_max = list()
791     nic_limit = 0
792     lnk_limit = 0
793     pci_limit = 0
794     for test_name, test_vals in y_vals.items():
795         try:
796             if test_vals[u"1"][1]:
797                 name = re.sub(
798                     REGEX_NIC,
799                     u"",
800                     test_name.replace(u'-ndrpdr', u'').replace(u'2n1l-', u'')
801                 )
802                 vals[name] = OrderedDict()
803                 y_val_1 = test_vals[u"1"][0] / 1e6
804                 y_val_2 = test_vals[u"2"][0] / 1e6 if test_vals[u"2"][0] \
805                     else None
806                 y_val_4 = test_vals[u"4"][0] / 1e6 if test_vals[u"4"][0] \
807                     else None
808
809                 vals[name][u"val"] = [y_val_1, y_val_2, y_val_4]
810                 vals[name][u"rel"] = [1.0, None, None]
811                 vals[name][u"ideal"] = [
812                     y_1c_max[test_name],
813                     y_1c_max[test_name] * 2,
814                     y_1c_max[test_name] * 4
815                 ]
816                 vals[name][u"diff"] = [
817                     (y_val_1 - y_1c_max[test_name]) * 100 / y_val_1, None, None
818                 ]
819                 vals[name][u"count"] = [
820                     test_vals[u"1"][1],
821                     test_vals[u"2"][1],
822                     test_vals[u"4"][1]
823                 ]
824
825                 try:
826                     val_max = max(vals[name][u"val"])
827                 except ValueError as err:
828                     logging.error(repr(err))
829                     continue
830                 if val_max:
831                     y_max.append(val_max)
832
833                 if y_val_2:
834                     vals[name][u"rel"][1] = round(y_val_2 / y_val_1, 2)
835                     vals[name][u"diff"][1] = \
836                         (y_val_2 - vals[name][u"ideal"][1]) * 100 / y_val_2
837                 if y_val_4:
838                     vals[name][u"rel"][2] = round(y_val_4 / y_val_1, 2)
839                     vals[name][u"diff"][2] = \
840                         (y_val_4 - vals[name][u"ideal"][2]) * 100 / y_val_4
841         except IndexError as err:
842             logging.warning(f"No data for {test_name}")
843             logging.warning(repr(err))
844
845         # Limits:
846         if u"x520" in test_name:
847             limit = plot[u"limits"][u"nic"][u"x520"]
848         elif u"x710" in test_name:
849             limit = plot[u"limits"][u"nic"][u"x710"]
850         elif u"xxv710" in test_name:
851             limit = plot[u"limits"][u"nic"][u"xxv710"]
852         elif u"xl710" in test_name:
853             limit = plot[u"limits"][u"nic"][u"xl710"]
854         elif u"x553" in test_name:
855             limit = plot[u"limits"][u"nic"][u"x553"]
856         elif u"cx556a" in test_name:
857             limit = plot[u"limits"][u"nic"][u"cx556a"]
858         else:
859             limit = 0
860         if limit > nic_limit:
861             nic_limit = limit
862
863         mul = 2 if u"ge2p" in test_name else 1
864         if u"10ge" in test_name:
865             limit = plot[u"limits"][u"link"][u"10ge"] * mul
866         elif u"25ge" in test_name:
867             limit = plot[u"limits"][u"link"][u"25ge"] * mul
868         elif u"40ge" in test_name:
869             limit = plot[u"limits"][u"link"][u"40ge"] * mul
870         elif u"100ge" in test_name:
871             limit = plot[u"limits"][u"link"][u"100ge"] * mul
872         else:
873             limit = 0
874         if limit > lnk_limit:
875             lnk_limit = limit
876
877         if u"cx556a" in test_name:
878             limit = plot[u"limits"][u"pci"][u"pci-g3-x8"]
879         else:
880             limit = plot[u"limits"][u"pci"][u"pci-g3-x16"]
881         if limit > pci_limit:
882             pci_limit = limit
883
884     traces = list()
885     annotations = list()
886     x_vals = [1, 2, 4]
887
888     # Limits:
889     if u"-gbps" not in plot_title and u"-cps-" not in plot_title:
890         nic_limit /= 1e6
891         lnk_limit /= 1e6
892         pci_limit /= 1e6
893         min_limit = min((nic_limit, lnk_limit, pci_limit))
894         if nic_limit == min_limit:
895             traces.append(plgo.Scatter(
896                 x=x_vals,
897                 y=[nic_limit, ] * len(x_vals),
898                 name=f"NIC: {nic_limit:.2f}Mpps",
899                 showlegend=False,
900                 mode=u"lines",
901                 line=dict(
902                     dash=u"dot",
903                     color=COLORS[-1],
904                     width=1),
905                 hoverinfo=u"none"
906             ))
907             annotations.append(dict(
908                 x=1,
909                 y=nic_limit,
910                 xref=u"x",
911                 yref=u"y",
912                 xanchor=u"left",
913                 yanchor=u"bottom",
914                 text=f"NIC: {nic_limit:.2f}Mpps",
915                 font=dict(
916                     size=14,
917                     color=COLORS[-1],
918                 ),
919                 align=u"left",
920                 showarrow=False
921             ))
922             y_max.append(nic_limit)
923         elif lnk_limit == min_limit:
924             traces.append(plgo.Scatter(
925                 x=x_vals,
926                 y=[lnk_limit, ] * len(x_vals),
927                 name=f"Link: {lnk_limit:.2f}Mpps",
928                 showlegend=False,
929                 mode=u"lines",
930                 line=dict(
931                     dash=u"dot",
932                     color=COLORS[-1],
933                     width=1),
934                 hoverinfo=u"none"
935             ))
936             annotations.append(dict(
937                 x=1,
938                 y=lnk_limit,
939                 xref=u"x",
940                 yref=u"y",
941                 xanchor=u"left",
942                 yanchor=u"bottom",
943                 text=f"Link: {lnk_limit:.2f}Mpps",
944                 font=dict(
945                     size=14,
946                     color=COLORS[-1],
947                 ),
948                 align=u"left",
949                 showarrow=False
950             ))
951             y_max.append(lnk_limit)
952         elif pci_limit == min_limit:
953             traces.append(plgo.Scatter(
954                 x=x_vals,
955                 y=[pci_limit, ] * len(x_vals),
956                 name=f"PCIe: {pci_limit:.2f}Mpps",
957                 showlegend=False,
958                 mode=u"lines",
959                 line=dict(
960                     dash=u"dot",
961                     color=COLORS[-1],
962                     width=1),
963                 hoverinfo=u"none"
964             ))
965             annotations.append(dict(
966                 x=1,
967                 y=pci_limit,
968                 xref=u"x",
969                 yref=u"y",
970                 xanchor=u"left",
971                 yanchor=u"bottom",
972                 text=f"PCIe: {pci_limit:.2f}Mpps",
973                 font=dict(
974                     size=14,
975                     color=COLORS[-1],
976                 ),
977                 align=u"left",
978                 showarrow=False
979             ))
980             y_max.append(pci_limit)
981
982     # Perfect and measured:
983     cidx = 0
984     for name, val in vals.items():
985         hovertext = list()
986         try:
987             for idx in range(len(val[u"val"])):
988                 htext = ""
989                 if isinstance(val[u"val"][idx], float):
990                     htext += (
991                         f"No. of Runs: {val[u'count'][idx]}<br>"
992                         f"Mean: {val[u'val'][idx]:.2f}{h_unit}<br>"
993                     )
994                 if isinstance(val[u"diff"][idx], float):
995                     htext += f"Diff: {round(val[u'diff'][idx]):.0f}%<br>"
996                 if isinstance(val[u"rel"][idx], float):
997                     htext += f"Speedup: {val[u'rel'][idx]:.2f}"
998                 hovertext.append(htext)
999             traces.append(
1000                 plgo.Scatter(
1001                     x=x_vals,
1002                     y=val[u"val"],
1003                     name=name,
1004                     legendgroup=name,
1005                     mode=u"lines+markers",
1006                     line=dict(
1007                         color=COLORS[cidx],
1008                         width=2),
1009                     marker=dict(
1010                         symbol=u"circle",
1011                         size=10
1012                     ),
1013                     text=hovertext,
1014                     hoverinfo=u"text+name"
1015                 )
1016             )
1017             traces.append(
1018                 plgo.Scatter(
1019                     x=x_vals,
1020                     y=val[u"ideal"],
1021                     name=f"{name} perfect",
1022                     legendgroup=name,
1023                     showlegend=False,
1024                     mode=u"lines",
1025                     line=dict(
1026                         color=COLORS[cidx],
1027                         width=2,
1028                         dash=u"dash"),
1029                     text=[f"Perfect: {y:.2f}Mpps" for y in val[u"ideal"]],
1030                     hoverinfo=u"text"
1031                 )
1032             )
1033             cidx += 1
1034         except (IndexError, ValueError, KeyError) as err:
1035             logging.warning(f"No data for {name}\n{repr(err)}")
1036
1037     try:
1038         # Create plot
1039         file_type = plot.get(u"output-file-type", u".html")
1040         logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
1041         layout = deepcopy(plot[u"layout"])
1042         if layout.get(u"title", None):
1043             layout[u"title"] = f"<b>Speedup Multi-core:</b> {layout[u'title']}"
1044         layout[u"yaxis"][u"range"] = [0, int(max(y_max) * 1.1)]
1045         layout[u"annotations"].extend(annotations)
1046         plpl = plgo.Figure(data=traces, layout=layout)
1047
1048         # Export Plot
1049         ploff.plot(
1050             plpl,
1051             show_link=False,
1052             auto_open=False,
1053             filename=f"{plot[u'output-file']}{file_type}"
1054         )
1055     except PlotlyError as err:
1056         logging.error(
1057             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1058         )
1059         return
1060
1061
1062 def plot_http_server_perf_box(plot, input_data):
1063     """Generate the plot(s) with algorithm: plot_http_server_perf_box
1064     specified in the specification file.
1065
1066     :param plot: Plot to generate.
1067     :param input_data: Data to process.
1068     :type plot: pandas.Series
1069     :type input_data: InputData
1070     """
1071
1072     # Transform the data
1073     logging.info(
1074         f"    Creating the data set for the {plot.get(u'type', u'')} "
1075         f"{plot.get(u'title', u'')}."
1076     )
1077     data = input_data.filter_data(plot)
1078     if data is None:
1079         logging.error(u"No data.")
1080         return
1081
1082     # Prepare the data for the plot
1083     y_vals = dict()
1084     for job in data:
1085         for build in job:
1086             for test in build:
1087                 if y_vals.get(test[u"name"], None) is None:
1088                     y_vals[test[u"name"]] = list()
1089                 try:
1090                     y_vals[test[u"name"]].append(test[u"result"])
1091                 except (KeyError, TypeError):
1092                     y_vals[test[u"name"]].append(None)
1093
1094     # Add None to the lists with missing data
1095     max_len = 0
1096     nr_of_samples = list()
1097     for val in y_vals.values():
1098         if len(val) > max_len:
1099             max_len = len(val)
1100         nr_of_samples.append(len(val))
1101     for val in y_vals.values():
1102         if len(val) < max_len:
1103             val.extend([None for _ in range(max_len - len(val))])
1104
1105     # Add plot traces
1106     traces = list()
1107     df_y = pd.DataFrame(y_vals)
1108     df_y.head()
1109     for i, col in enumerate(df_y.columns):
1110         name = \
1111             f"{i + 1}. " \
1112             f"({nr_of_samples[i]:02d} " \
1113             f"run{u's' if nr_of_samples[i] > 1 else u''}) " \
1114             f"{col.lower().replace(u'-ndrpdr', u'')}"
1115         if len(name) > 50:
1116             name_lst = name.split(u'-')
1117             name = u""
1118             split_name = True
1119             for segment in name_lst:
1120                 if (len(name) + len(segment) + 1) > 50 and split_name:
1121                     name += u"<br>    "
1122                     split_name = False
1123                 name += segment + u'-'
1124             name = name[:-1]
1125
1126         traces.append(plgo.Box(x=[str(i + 1) + u'.'] * len(df_y[col]),
1127                                y=df_y[col],
1128                                name=name,
1129                                **plot[u"traces"]))
1130     try:
1131         # Create plot
1132         plpl = plgo.Figure(data=traces, layout=plot[u"layout"])
1133
1134         # Export Plot
1135         logging.info(
1136             f"    Writing file {plot[u'output-file']}"
1137             f"{plot[u'output-file-type']}."
1138         )
1139         ploff.plot(
1140             plpl,
1141             show_link=False,
1142             auto_open=False,
1143             filename=f"{plot[u'output-file']}{plot[u'output-file-type']}"
1144         )
1145     except PlotlyError as err:
1146         logging.error(
1147             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1148         )
1149         return
1150
1151
1152 def plot_nf_heatmap(plot, input_data):
1153     """Generate the plot(s) with algorithm: plot_nf_heatmap
1154     specified in the specification file.
1155
1156     :param plot: Plot to generate.
1157     :param input_data: Data to process.
1158     :type plot: pandas.Series
1159     :type input_data: InputData
1160     """
1161
1162     regex_cn = re.compile(r'^(\d*)R(\d*)C$')
1163     regex_test_name = re.compile(r'^.*-(\d+ch|\d+pl)-'
1164                                  r'(\d+mif|\d+vh)-'
1165                                  r'(\d+vm\d+t|\d+dcr\d+t|\d+dcr\d+c).*$')
1166     vals = dict()
1167
1168     # Transform the data
1169     logging.info(
1170         f"    Creating the data set for the {plot.get(u'type', u'')} "
1171         f"{plot.get(u'title', u'')}."
1172     )
1173     data = input_data.filter_data(plot, continue_on_error=True)
1174     if data is None or data.empty:
1175         logging.error(u"No data.")
1176         return
1177
1178     for job in data:
1179         for build in job:
1180             for test in build:
1181                 for tag in test[u"tags"]:
1182                     groups = re.search(regex_cn, tag)
1183                     if groups:
1184                         chain = str(groups.group(1))
1185                         node = str(groups.group(2))
1186                         break
1187                 else:
1188                     continue
1189                 groups = re.search(regex_test_name, test[u"name"])
1190                 if groups and len(groups.groups()) == 3:
1191                     hover_name = (
1192                         f"{str(groups.group(1))}-"
1193                         f"{str(groups.group(2))}-"
1194                         f"{str(groups.group(3))}"
1195                     )
1196                 else:
1197                     hover_name = u""
1198                 if vals.get(chain, None) is None:
1199                     vals[chain] = dict()
1200                 if vals[chain].get(node, None) is None:
1201                     vals[chain][node] = dict(
1202                         name=hover_name,
1203                         vals=list(),
1204                         nr=None,
1205                         mean=None,
1206                         stdev=None
1207                     )
1208                 try:
1209                     if plot[u"include-tests"] == u"MRR":
1210                         result = test[u"result"][u"receive-rate"]
1211                     elif plot[u"include-tests"] == u"PDR":
1212                         result = test[u"throughput"][u"PDR"][u"LOWER"]
1213                     elif plot[u"include-tests"] == u"NDR":
1214                         result = test[u"throughput"][u"NDR"][u"LOWER"]
1215                     else:
1216                         result = None
1217                 except TypeError:
1218                     result = None
1219
1220                 if result:
1221                     vals[chain][node][u"vals"].append(result)
1222
1223     if not vals:
1224         logging.error(u"No data.")
1225         return
1226
1227     txt_chains = list()
1228     txt_nodes = list()
1229     for key_c in vals:
1230         txt_chains.append(key_c)
1231         for key_n in vals[key_c].keys():
1232             txt_nodes.append(key_n)
1233             if vals[key_c][key_n][u"vals"]:
1234                 vals[key_c][key_n][u"nr"] = len(vals[key_c][key_n][u"vals"])
1235                 vals[key_c][key_n][u"mean"] = \
1236                     round(mean(vals[key_c][key_n][u"vals"]) / 1000000, 1)
1237                 vals[key_c][key_n][u"stdev"] = \
1238                     round(stdev(vals[key_c][key_n][u"vals"]) / 1000000, 1)
1239     txt_nodes = list(set(txt_nodes))
1240
1241     def sort_by_int(value):
1242         """Makes possible to sort a list of strings which represent integers.
1243
1244         :param value: Integer as a string.
1245         :type value: str
1246         :returns: Integer representation of input parameter 'value'.
1247         :rtype: int
1248         """
1249         return int(value)
1250
1251     txt_chains = sorted(txt_chains, key=sort_by_int)
1252     txt_nodes = sorted(txt_nodes, key=sort_by_int)
1253
1254     chains = [i + 1 for i in range(len(txt_chains))]
1255     nodes = [i + 1 for i in range(len(txt_nodes))]
1256
1257     data = [list() for _ in range(len(chains))]
1258     for chain in chains:
1259         for node in nodes:
1260             try:
1261                 val = vals[txt_chains[chain - 1]][txt_nodes[node - 1]][u"mean"]
1262             except (KeyError, IndexError):
1263                 val = None
1264             data[chain - 1].append(val)
1265
1266     # Color scales:
1267     my_green = [[0.0, u"rgb(235, 249, 242)"],
1268                 [1.0, u"rgb(45, 134, 89)"]]
1269
1270     my_blue = [[0.0, u"rgb(236, 242, 248)"],
1271                [1.0, u"rgb(57, 115, 172)"]]
1272
1273     my_grey = [[0.0, u"rgb(230, 230, 230)"],
1274                [1.0, u"rgb(102, 102, 102)"]]
1275
1276     hovertext = list()
1277     annotations = list()
1278
1279     text = (u"Test: {name}<br>"
1280             u"Runs: {nr}<br>"
1281             u"Thput: {val}<br>"
1282             u"StDev: {stdev}")
1283
1284     for chain, _ in enumerate(txt_chains):
1285         hover_line = list()
1286         for node, _ in enumerate(txt_nodes):
1287             if data[chain][node] is not None:
1288                 annotations.append(
1289                     dict(
1290                         x=node+1,
1291                         y=chain+1,
1292                         xref=u"x",
1293                         yref=u"y",
1294                         xanchor=u"center",
1295                         yanchor=u"middle",
1296                         text=str(data[chain][node]),
1297                         font=dict(
1298                             size=14,
1299                         ),
1300                         align=u"center",
1301                         showarrow=False
1302                     )
1303                 )
1304                 hover_line.append(text.format(
1305                     name=vals[txt_chains[chain]][txt_nodes[node]][u"name"],
1306                     nr=vals[txt_chains[chain]][txt_nodes[node]][u"nr"],
1307                     val=data[chain][node],
1308                     stdev=vals[txt_chains[chain]][txt_nodes[node]][u"stdev"]))
1309         hovertext.append(hover_line)
1310
1311     traces = [
1312         plgo.Heatmap(
1313             x=nodes,
1314             y=chains,
1315             z=data,
1316             colorbar=dict(
1317                 title=plot.get(u"z-axis", u""),
1318                 titleside=u"right",
1319                 titlefont=dict(
1320                     size=16
1321                 ),
1322                 tickfont=dict(
1323                     size=16,
1324                 ),
1325                 tickformat=u".1f",
1326                 yanchor=u"bottom",
1327                 y=-0.02,
1328                 len=0.925,
1329             ),
1330             showscale=True,
1331             colorscale=my_green,
1332             text=hovertext,
1333             hoverinfo=u"text"
1334         )
1335     ]
1336
1337     for idx, item in enumerate(txt_nodes):
1338         # X-axis, numbers:
1339         annotations.append(
1340             dict(
1341                 x=idx+1,
1342                 y=0.05,
1343                 xref=u"x",
1344                 yref=u"y",
1345                 xanchor=u"center",
1346                 yanchor=u"top",
1347                 text=item,
1348                 font=dict(
1349                     size=16,
1350                 ),
1351                 align=u"center",
1352                 showarrow=False
1353             )
1354         )
1355     for idx, item in enumerate(txt_chains):
1356         # Y-axis, numbers:
1357         annotations.append(
1358             dict(
1359                 x=0.35,
1360                 y=idx+1,
1361                 xref=u"x",
1362                 yref=u"y",
1363                 xanchor=u"right",
1364                 yanchor=u"middle",
1365                 text=item,
1366                 font=dict(
1367                     size=16,
1368                 ),
1369                 align=u"center",
1370                 showarrow=False
1371             )
1372         )
1373     # X-axis, title:
1374     annotations.append(
1375         dict(
1376             x=0.55,
1377             y=-0.15,
1378             xref=u"paper",
1379             yref=u"y",
1380             xanchor=u"center",
1381             yanchor=u"bottom",
1382             text=plot.get(u"x-axis", u""),
1383             font=dict(
1384                 size=16,
1385             ),
1386             align=u"center",
1387             showarrow=False
1388         )
1389     )
1390     # Y-axis, title:
1391     annotations.append(
1392         dict(
1393             x=-0.1,
1394             y=0.5,
1395             xref=u"x",
1396             yref=u"paper",
1397             xanchor=u"center",
1398             yanchor=u"middle",
1399             text=plot.get(u"y-axis", u""),
1400             font=dict(
1401                 size=16,
1402             ),
1403             align=u"center",
1404             textangle=270,
1405             showarrow=False
1406         )
1407     )
1408     updatemenus = list([
1409         dict(
1410             x=1.0,
1411             y=0.0,
1412             xanchor=u"right",
1413             yanchor=u"bottom",
1414             direction=u"up",
1415             buttons=list([
1416                 dict(
1417                     args=[
1418                         {
1419                             u"colorscale": [my_green, ],
1420                             u"reversescale": False
1421                         }
1422                     ],
1423                     label=u"Green",
1424                     method=u"update"
1425                 ),
1426                 dict(
1427                     args=[
1428                         {
1429                             u"colorscale": [my_blue, ],
1430                             u"reversescale": False
1431                         }
1432                     ],
1433                     label=u"Blue",
1434                     method=u"update"
1435                 ),
1436                 dict(
1437                     args=[
1438                         {
1439                             u"colorscale": [my_grey, ],
1440                             u"reversescale": False
1441                         }
1442                     ],
1443                     label=u"Grey",
1444                     method=u"update"
1445                 )
1446             ])
1447         )
1448     ])
1449
1450     try:
1451         layout = deepcopy(plot[u"layout"])
1452     except KeyError as err:
1453         logging.error(f"Finished with error: No layout defined\n{repr(err)}")
1454         return
1455
1456     layout[u"annotations"] = annotations
1457     layout[u'updatemenus'] = updatemenus
1458
1459     try:
1460         # Create plot
1461         plpl = plgo.Figure(data=traces, layout=layout)
1462
1463         # Export Plot
1464         logging.info(f"    Writing file {plot[u'output-file']}.html")
1465         ploff.plot(
1466             plpl,
1467             show_link=False,
1468             auto_open=False,
1469             filename=f"{plot[u'output-file']}.html"
1470         )
1471     except PlotlyError as err:
1472         logging.error(
1473             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1474         )
1475         return