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