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