CSIT-1350: Add new data to 1810 report
[csit.git] / resources / tools / presentation / generator_plots.py
1 # Copyright (c) 2018 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 logging
19 import pandas as pd
20 import plotly.offline as ploff
21 import plotly.graph_objs as plgo
22
23 from plotly.exceptions import PlotlyError
24 from collections import OrderedDict
25 from copy import deepcopy
26
27 from utils import mean
28
29
30 COLORS = ["SkyBlue", "Olive", "Purple", "Coral", "Indigo", "Pink",
31           "Chocolate", "Brown", "Magenta", "Cyan", "Orange", "Black",
32           "Violet", "Blue", "Yellow", "BurlyWood", "CadetBlue", "Crimson",
33           "DarkBlue", "DarkCyan", "DarkGreen", "Green", "GoldenRod",
34           "LightGreen", "LightSeaGreen", "LightSkyBlue", "Maroon",
35           "MediumSeaGreen", "SeaGreen", "LightSlateGrey"]
36
37
38 def generate_plots(spec, data):
39     """Generate all plots specified in the specification file.
40
41     :param spec: Specification read from the specification file.
42     :param data: Data to process.
43     :type spec: Specification
44     :type data: InputData
45     """
46
47     logging.info("Generating the plots ...")
48     for index, plot in enumerate(spec.plots):
49         try:
50             logging.info("  Plot nr {0}: {1}".format(index + 1,
51                                                      plot.get("title", "")))
52             plot["limits"] = spec.configuration["limits"]
53             eval(plot["algorithm"])(plot, data)
54             logging.info("  Done.")
55         except NameError as err:
56             logging.error("Probably algorithm '{alg}' is not defined: {err}".
57                           format(alg=plot["algorithm"], err=repr(err)))
58     logging.info("Done.")
59
60
61 def plot_performance_box(plot, input_data):
62     """Generate the plot(s) with algorithm: plot_performance_box
63     specified in the specification file.
64
65     :param plot: Plot to generate.
66     :param input_data: Data to process.
67     :type plot: pandas.Series
68     :type input_data: InputData
69     """
70
71     # Transform the data
72     plot_title = plot.get("title", "")
73     logging.info("    Creating the data set for the {0} '{1}'.".
74                  format(plot.get("type", ""), plot_title))
75     data = input_data.filter_data(plot)
76     if data is None:
77         logging.error("No data.")
78         return
79
80     # Prepare the data for the plot
81     y_vals = dict()
82     y_tags = dict()
83     for job in data:
84         for build in job:
85             for test in build:
86                 if y_vals.get(test["parent"], None) is None:
87                     y_vals[test["parent"]] = list()
88                     y_tags[test["parent"]] = test.get("tags", None)
89                 try:
90                     if test["type"] in ("NDRPDR", ):
91                         if "-pdr" in plot_title.lower():
92                             y_vals[test["parent"]].\
93                                 append(test["throughput"]["PDR"]["LOWER"])
94                         elif "-ndr" in plot_title.lower():
95                             y_vals[test["parent"]]. \
96                                 append(test["throughput"]["NDR"]["LOWER"])
97                         else:
98                             continue
99                     else:
100                         continue
101                 except (KeyError, TypeError):
102                     y_vals[test["parent"]].append(None)
103
104     # Sort the tests
105     order = plot.get("sort", None)
106     if order and y_tags:
107         y_sorted = OrderedDict()
108         y_tags_l = {s: [t.lower() for t in ts] for s, ts in y_tags.items()}
109         for tag in order:
110             logging.info(tag)
111             for suite, tags in y_tags_l.items():
112                 if "not " in tag:
113                     tag = tag.split(" ")[-1]
114                     if tag.lower() in tags:
115                         continue
116                 else:
117                     if tag.lower() not in tags:
118                         continue
119                 try:
120                     y_sorted[suite] = y_vals.pop(suite)
121                     y_tags_l.pop(suite)
122                     logging.info(suite)
123                 except KeyError as err:
124                     logging.error("Not found: {0}".format(err))
125                 finally:
126                     break
127     else:
128         y_sorted = y_vals
129
130     # Add None to the lists with missing data
131     max_len = 0
132     for val in y_sorted.values():
133         if len(val) > max_len:
134             max_len = len(val)
135     for key, val in y_sorted.items():
136         if len(val) < max_len:
137             val.extend([None for _ in range(max_len - len(val))])
138
139     # Add plot traces
140     traces = list()
141     df = pd.DataFrame(y_sorted)
142     df.head()
143     y_max = list()
144     for i, col in enumerate(df.columns):
145         name = "{0}. {1}".format(i + 1, col.lower().replace('-ndrpdrdisc', '').
146                                  replace('-ndrpdr', ''))
147         logging.info(name)
148         traces.append(plgo.Box(x=[str(i + 1) + '.'] * len(df[col]),
149                                y=[y / 1000000 if y else None for y in df[col]],
150                                name=name,
151                                **plot["traces"]))
152         try:
153             val_max = max(df[col])
154         except ValueError as err:
155             logging.error(err)
156             continue
157         if val_max:
158             y_max.append(int(val_max / 1000000) + 1)
159
160     try:
161         # Create plot
162         layout = deepcopy(plot["layout"])
163         if layout.get("title", None):
164             layout["title"] = "<b>Packet Throughput:</b> {0}". \
165                 format(layout["title"])
166         if y_max:
167             layout["yaxis"]["range"] = [0, max(y_max)]
168         plpl = plgo.Figure(data=traces, layout=layout)
169
170         # Export Plot
171         logging.info("    Writing file '{0}{1}'.".
172                      format(plot["output-file"], plot["output-file-type"]))
173         ploff.plot(plpl, show_link=False, auto_open=False,
174                    filename='{0}{1}'.format(plot["output-file"],
175                                             plot["output-file-type"]))
176     except PlotlyError as err:
177         logging.error("   Finished with error: {}".
178                       format(str(err).replace("\n", " ")))
179         return
180
181
182 def plot_latency_error_bars(plot, input_data):
183     """Generate the plot(s) with algorithm: plot_latency_error_bars
184     specified in the specification file.
185
186     :param plot: Plot to generate.
187     :param input_data: Data to process.
188     :type plot: pandas.Series
189     :type input_data: InputData
190     """
191
192     # Transform the data
193     plot_title = plot.get("title", "")
194     logging.info("    Creating the data set for the {0} '{1}'.".
195                  format(plot.get("type", ""), plot_title))
196     data = input_data.filter_data(plot)
197     if data is None:
198         logging.error("No data.")
199         return
200
201     # Prepare the data for the plot
202     y_tmp_vals = dict()
203     y_tags = dict()
204     for job in data:
205         for build in job:
206             for test in build:
207                 if y_tmp_vals.get(test["parent"], None) is None:
208                     y_tmp_vals[test["parent"]] = [
209                         list(),  # direction1, min
210                         list(),  # direction1, avg
211                         list(),  # direction1, max
212                         list(),  # direction2, min
213                         list(),  # direction2, avg
214                         list()   # direction2, max
215                     ]
216                     y_tags[test["parent"]] = test.get("tags", None)
217                 try:
218                     if test["type"] in ("NDRPDR", ):
219                         if "-pdr" in plot_title.lower():
220                             ttype = "PDR"
221                         elif "-ndr" in plot_title.lower():
222                             ttype = "NDR"
223                         else:
224                             continue
225                         y_tmp_vals[test["parent"]][0].append(
226                             test["latency"][ttype]["direction1"]["min"])
227                         y_tmp_vals[test["parent"]][1].append(
228                             test["latency"][ttype]["direction1"]["avg"])
229                         y_tmp_vals[test["parent"]][2].append(
230                             test["latency"][ttype]["direction1"]["max"])
231                         y_tmp_vals[test["parent"]][3].append(
232                             test["latency"][ttype]["direction2"]["min"])
233                         y_tmp_vals[test["parent"]][4].append(
234                             test["latency"][ttype]["direction2"]["avg"])
235                         y_tmp_vals[test["parent"]][5].append(
236                             test["latency"][ttype]["direction2"]["max"])
237                     else:
238                         continue
239                 except (KeyError, TypeError):
240                     pass
241
242     # Sort the tests
243     order = plot.get("sort", None)
244     if order and y_tags:
245         y_sorted = OrderedDict()
246         y_tags_l = {s: [t.lower() for t in ts] for s, ts in y_tags.items()}
247         for tag in order:
248             logging.info(tag)
249             for suite, tags in y_tags_l.items():
250                 if "not " in tag:
251                     tag = tag.split(" ")[-1]
252                     if tag.lower() in tags:
253                         continue
254                 else:
255                     if tag.lower() not in tags:
256                         continue
257                 try:
258                     y_sorted[suite] = y_tmp_vals.pop(suite)
259                     y_tags_l.pop(suite)
260                     logging.info(suite)
261                 except KeyError as err:
262                     logging.error("Not found: {0}".format(err))
263                 finally:
264                     break
265     else:
266         y_sorted = y_tmp_vals
267
268     x_vals = list()
269     y_vals = list()
270     y_mins = list()
271     y_maxs = list()
272     for key, val in y_sorted.items():
273         key = "-".join(key.split("-")[1:-1])
274         x_vals.append(key)  # dir 1
275         y_vals.append(mean(val[1]) if val[1] else None)
276         y_mins.append(mean(val[0]) if val[0] else None)
277         y_maxs.append(mean(val[2]) if val[2] else None)
278         x_vals.append(key)  # dir 2
279         y_vals.append(mean(val[4]) if val[4] else None)
280         y_mins.append(mean(val[3]) if val[3] else None)
281         y_maxs.append(mean(val[5]) if val[5] else None)
282
283     traces = list()
284     annotations = list()
285
286     for idx in range(len(x_vals)):
287         if not bool(int(idx % 2)):
288             direction = "West - East"
289         else:
290             direction = "East - West"
291         hovertext = ("Test: {test}<br>"
292                      "Direction: {dir}<br>".format(test=x_vals[idx],
293                                                    dir=direction))
294         if isinstance(y_maxs[idx], float):
295             hovertext += "Max: {max:.2f}uSec<br>".format(max=y_maxs[idx])
296         if isinstance(y_vals[idx], float):
297             hovertext += "Avg: {avg:.2f}uSec<br>".format(avg=y_vals[idx])
298         if isinstance(y_mins[idx], float):
299             hovertext += "Min: {min:.2f}uSec".format(min=y_mins[idx])
300
301         if isinstance(y_maxs[idx], float) and isinstance(y_vals[idx], float):
302             array = [y_maxs[idx] - y_vals[idx], ]
303         else:
304             array = [None, ]
305         if isinstance(y_mins[idx], float) and isinstance(y_vals[idx], float):
306             arrayminus = [y_vals[idx] - y_mins[idx], ]
307         else:
308             arrayminus = [None, ]
309         traces.append(plgo.Scatter(
310             x=[idx, ],
311             y=[y_vals[idx], ],
312             name=x_vals[idx],
313             legendgroup=x_vals[idx],
314             showlegend=bool(int(idx % 2)),
315             mode="markers",
316             error_y=dict(
317                 type='data',
318                 symmetric=False,
319                 array=array,
320                 arrayminus=arrayminus,
321                 color=COLORS[int(idx / 2)]
322             ),
323             marker=dict(
324                 size=10,
325                 color=COLORS[int(idx / 2)],
326             ),
327             text=hovertext,
328             hoverinfo="text",
329         ))
330         annotations.append(dict(
331             x=idx,
332             y=0,
333             xref="x",
334             yref="y",
335             xanchor="center",
336             yanchor="top",
337             text="E-W" if bool(int(idx % 2)) else "W-E",
338             font=dict(
339                 size=16,
340             ),
341             align="center",
342             showarrow=False
343         ))
344
345     try:
346         # Create plot
347         logging.info("    Writing file '{0}{1}'.".
348                      format(plot["output-file"], plot["output-file-type"]))
349         layout = deepcopy(plot["layout"])
350         if layout.get("title", None):
351             layout["title"] = "<b>Packet Latency:</b> {0}".\
352                 format(layout["title"])
353         layout["annotations"] = annotations
354         plpl = plgo.Figure(data=traces, layout=layout)
355
356         # Export Plot
357         ploff.plot(plpl,
358                    show_link=False, auto_open=False,
359                    filename='{0}{1}'.format(plot["output-file"],
360                                             plot["output-file-type"]))
361     except PlotlyError as err:
362         logging.error("   Finished with error: {}".
363                       format(str(err).replace("\n", " ")))
364         return
365
366
367 def plot_throughput_speedup_analysis(plot, input_data):
368     """Generate the plot(s) with algorithm:
369     plot_throughput_speedup_analysis
370     specified in the specification file.
371
372     :param plot: Plot to generate.
373     :param input_data: Data to process.
374     :type plot: pandas.Series
375     :type input_data: InputData
376     """
377
378     # Transform the data
379     plot_title = plot.get("title", "")
380     logging.info("    Creating the data set for the {0} '{1}'.".
381                  format(plot.get("type", ""), plot_title))
382     data = input_data.filter_data(plot)
383     if data is None:
384         logging.error("No data.")
385         return
386
387     y_vals = dict()
388     y_tags = dict()
389     for job in data:
390         for build in job:
391             for test in build:
392                 if y_vals.get(test["parent"], None) is None:
393                     y_vals[test["parent"]] = {"1": list(),
394                                               "2": list(),
395                                               "4": list()}
396                     y_tags[test["parent"]] = test.get("tags", None)
397                 try:
398                     if test["type"] in ("NDRPDR",):
399                         if "-pdr" in plot_title.lower():
400                             ttype = "PDR"
401                         elif "-ndr" in plot_title.lower():
402                             ttype = "NDR"
403                         else:
404                             continue
405                         if "1C" in test["tags"]:
406                             y_vals[test["parent"]]["1"]. \
407                                 append(test["throughput"][ttype]["LOWER"])
408                         elif "2C" in test["tags"]:
409                             y_vals[test["parent"]]["2"]. \
410                                 append(test["throughput"][ttype]["LOWER"])
411                         elif "4C" in test["tags"]:
412                             y_vals[test["parent"]]["4"]. \
413                                 append(test["throughput"][ttype]["LOWER"])
414                 except (KeyError, TypeError):
415                     pass
416
417     if not y_vals:
418         logging.warning("No data for the plot '{}'".
419                         format(plot.get("title", "")))
420         return
421
422     y_1c_max = dict()
423     for test_name, test_vals in y_vals.items():
424         for key, test_val in test_vals.items():
425             if test_val:
426                 y_vals[test_name][key] = sum(test_val) / len(test_val)
427                 if key == "1":
428                     y_1c_max[test_name] = max(test_val) / 1000000.0
429
430     vals = dict()
431     y_max = list()
432     nic_limit = 0
433     lnk_limit = 0
434     pci_limit = plot["limits"]["pci"]["pci-g3-x8"]
435     for test_name, test_vals in y_vals.items():
436         if test_vals["1"]:
437             name = "-".join(test_name.split('-')[1:-1])
438
439             vals[name] = dict()
440             y_val_1 = test_vals["1"] / 1000000.0
441             y_val_2 = test_vals["2"] / 1000000.0 if test_vals["2"] else None
442             y_val_4 = test_vals["4"] / 1000000.0 if test_vals["4"] else None
443
444             vals[name]["val"] = [y_val_1, y_val_2, y_val_4]
445             vals[name]["rel"] = [1.0, None, None]
446             vals[name]["ideal"] = [y_1c_max[test_name],
447                                    y_1c_max[test_name] * 2,
448                                    y_1c_max[test_name] * 4]
449             vals[name]["diff"] = \
450                 [(y_val_1 - y_1c_max[test_name]) * 100 / y_val_1, None, None]
451
452             try:
453                 val_max = max(max(vals[name]["val"], vals[name]["ideal"]))
454             except ValueError as err:
455                 logging.error(err)
456                 continue
457             if val_max:
458                 y_max.append(int((val_max / 10) + 1) * 10)
459
460             if y_val_2:
461                 vals[name]["rel"][1] = round(y_val_2 / y_val_1, 2)
462                 vals[name]["diff"][1] = \
463                     (y_val_2 - vals[name]["ideal"][1]) * 100 / y_val_2
464             if y_val_4:
465                 vals[name]["rel"][2] = round(y_val_4 / y_val_1, 2)
466                 vals[name]["diff"][2] = \
467                     (y_val_4 - vals[name]["ideal"][2]) * 100 / y_val_4
468
469         # Limits:
470         if "x520" in test_name:
471             limit = plot["limits"]["nic"]["x520"]
472         elif "x710" in test_name:
473             limit = plot["limits"]["nic"]["x710"]
474         elif "xxv710" in test_name:
475             limit = plot["limits"]["nic"]["xxv710"]
476         elif "xl710" in test_name:
477             limit = plot["limits"]["nic"]["xl710"]
478         else:
479             limit = 0
480         if limit > nic_limit:
481             nic_limit = limit
482
483         mul = 2 if "ge2p" in test_name else 1
484         if "10ge" in test_name:
485             limit = plot["limits"]["link"]["10ge"] * mul
486         elif "25ge" in test_name:
487             limit = plot["limits"]["link"]["25ge"] * mul
488         elif "40ge" in test_name:
489             limit = plot["limits"]["link"]["40ge"] * mul
490         elif "100ge" in test_name:
491             limit = plot["limits"]["link"]["100ge"] * mul
492         else:
493             limit = 0
494         if limit > lnk_limit:
495             lnk_limit = limit
496
497     # Sort the tests
498     order = plot.get("sort", None)
499     if order and y_tags:
500         y_sorted = OrderedDict()
501         y_tags_l = {s: [t.lower() for t in ts] for s, ts in y_tags.items()}
502         for tag in order:
503             for test, tags in y_tags_l.items():
504                 if tag.lower() in tags:
505                     name = "-".join(test.split('-')[1:-1])
506                     try:
507                         y_sorted[name] = vals.pop(name)
508                         y_tags_l.pop(test)
509                     except KeyError as err:
510                         logging.error("Not found: {0}".format(err))
511                     finally:
512                         break
513     else:
514         y_sorted = vals
515
516     traces = list()
517     annotations = list()
518     x_vals = [1, 2, 4]
519
520     # Limits:
521     try:
522         threshold = 1.1 * max(y_max)  # 10%
523     except ValueError as err:
524         logging.error(err)
525         return
526     nic_limit /= 1000000.0
527     if nic_limit < threshold:
528         traces.append(plgo.Scatter(
529             x=x_vals,
530             y=[nic_limit, ] * len(x_vals),
531             name="NIC: {0:.2f}Mpps".format(nic_limit),
532             showlegend=False,
533             mode="lines",
534             line=dict(
535                 dash="dot",
536                 color=COLORS[-1],
537                 width=1),
538             hoverinfo="none"
539         ))
540         annotations.append(dict(
541             x=1,
542             y=nic_limit,
543             xref="x",
544             yref="y",
545             xanchor="left",
546             yanchor="bottom",
547             text="NIC: {0:.2f}Mpps".format(nic_limit),
548             font=dict(
549                 size=14,
550                 color=COLORS[-1],
551             ),
552             align="left",
553             showarrow=False
554         ))
555         y_max.append(int((nic_limit / 10) + 1) * 10)
556
557     lnk_limit /= 1000000.0
558     if lnk_limit < threshold:
559         traces.append(plgo.Scatter(
560             x=x_vals,
561             y=[lnk_limit, ] * len(x_vals),
562             name="Link: {0:.2f}Mpps".format(lnk_limit),
563             showlegend=False,
564             mode="lines",
565             line=dict(
566                 dash="dot",
567                 color=COLORS[-2],
568                 width=1),
569             hoverinfo="none"
570         ))
571         annotations.append(dict(
572             x=1,
573             y=lnk_limit,
574             xref="x",
575             yref="y",
576             xanchor="left",
577             yanchor="bottom",
578             text="Link: {0:.2f}Mpps".format(lnk_limit),
579             font=dict(
580                 size=14,
581                 color=COLORS[-2],
582             ),
583             align="left",
584             showarrow=False
585         ))
586         y_max.append(int((lnk_limit / 10) + 1) * 10)
587
588     pci_limit /= 1000000.0
589     if pci_limit < threshold:
590         traces.append(plgo.Scatter(
591             x=x_vals,
592             y=[pci_limit, ] * len(x_vals),
593             name="PCIe: {0:.2f}Mpps".format(pci_limit),
594             showlegend=False,
595             mode="lines",
596             line=dict(
597                 dash="dot",
598                 color=COLORS[-3],
599                 width=1),
600             hoverinfo="none"
601         ))
602         annotations.append(dict(
603             x=1,
604             y=pci_limit,
605             xref="x",
606             yref="y",
607             xanchor="left",
608             yanchor="bottom",
609             text="PCIe: {0:.2f}Mpps".format(pci_limit),
610             font=dict(
611                 size=14,
612                 color=COLORS[-3],
613             ),
614             align="left",
615             showarrow=False
616         ))
617         y_max.append(int((pci_limit / 10) + 1) * 10)
618
619     # Perfect and measured:
620     cidx = 0
621     for name, val in y_sorted.iteritems():
622         hovertext = list()
623         for idx in range(len(val["val"])):
624             htext = ""
625             if isinstance(val["val"][idx], float):
626                 htext += "value: {0:.2f}Mpps<br>".format(val["val"][idx])
627             if isinstance(val["diff"][idx], float):
628                 htext += "diff: {0:.0f}%<br>".format(round(val["diff"][idx]))
629             if isinstance(val["rel"][idx], float):
630                 htext += "speedup: {0:.2f}".format(val["rel"][idx])
631             hovertext.append(htext)
632         traces.append(plgo.Scatter(x=x_vals,
633                                    y=val["val"],
634                                    name=name,
635                                    legendgroup=name,
636                                    mode="lines+markers",
637                                    line=dict(
638                                        color=COLORS[cidx],
639                                        width=2),
640                                    marker=dict(
641                                        symbol="circle",
642                                        size=10
643                                    ),
644                                    text=hovertext,
645                                    hoverinfo="text+name"
646                                    ))
647         traces.append(plgo.Scatter(x=x_vals,
648                                    y=val["ideal"],
649                                    name="{0} perfect".format(name),
650                                    legendgroup=name,
651                                    showlegend=False,
652                                    mode="lines",
653                                    line=dict(
654                                        color=COLORS[cidx],
655                                        width=2,
656                                        dash="dash"),
657                                    text=["perfect: {0:.2f}Mpps".format(y)
658                                          for y in val["ideal"]],
659                                    hoverinfo="text"
660                                    ))
661         cidx += 1
662
663     try:
664         # Create plot
665         logging.info("    Writing file '{0}{1}'.".
666                      format(plot["output-file"], plot["output-file-type"]))
667         layout = deepcopy(plot["layout"])
668         if layout.get("title", None):
669             layout["title"] = "<b>Speedup Multi-core:</b> {0}". \
670                 format(layout["title"])
671         layout["annotations"].extend(annotations)
672         plpl = plgo.Figure(data=traces, layout=layout)
673
674         # Export Plot
675         ploff.plot(plpl,
676                    show_link=False, auto_open=False,
677                    filename='{0}{1}'.format(plot["output-file"],
678                                             plot["output-file-type"]))
679     except PlotlyError as err:
680         logging.error("   Finished with error: {}".
681                       format(str(err).replace("\n", " ")))
682         return
683
684
685 def plot_http_server_performance_box(plot, input_data):
686     """Generate the plot(s) with algorithm: plot_http_server_performance_box
687     specified in the specification file.
688
689     :param plot: Plot to generate.
690     :param input_data: Data to process.
691     :type plot: pandas.Series
692     :type input_data: InputData
693     """
694
695     # Transform the data
696     logging.info("    Creating the data set for the {0} '{1}'.".
697                  format(plot.get("type", ""), plot.get("title", "")))
698     data = input_data.filter_data(plot)
699     if data is None:
700         logging.error("No data.")
701         return
702
703     # Prepare the data for the plot
704     y_vals = dict()
705     for job in data:
706         for build in job:
707             for test in build:
708                 if y_vals.get(test["name"], None) is None:
709                     y_vals[test["name"]] = list()
710                 try:
711                     y_vals[test["name"]].append(test["result"])
712                 except (KeyError, TypeError):
713                     y_vals[test["name"]].append(None)
714
715     # Add None to the lists with missing data
716     max_len = 0
717     for val in y_vals.values():
718         if len(val) > max_len:
719             max_len = len(val)
720     for key, val in y_vals.items():
721         if len(val) < max_len:
722             val.extend([None for _ in range(max_len - len(val))])
723
724     # Add plot traces
725     traces = list()
726     df = pd.DataFrame(y_vals)
727     df.head()
728     for i, col in enumerate(df.columns):
729         name = "{0}. {1}".format(i + 1, col.lower().replace('-cps', '').
730                                  replace('-rps', ''))
731         traces.append(plgo.Box(x=[str(i + 1) + '.'] * len(df[col]),
732                                y=df[col],
733                                name=name,
734                                **plot["traces"]))
735     try:
736         # Create plot
737         plpl = plgo.Figure(data=traces, layout=plot["layout"])
738
739         # Export Plot
740         logging.info("    Writing file '{0}{1}'.".
741                      format(plot["output-file"], plot["output-file-type"]))
742         ploff.plot(plpl, show_link=False, auto_open=False,
743                    filename='{0}{1}'.format(plot["output-file"],
744                                             plot["output-file-type"]))
745     except PlotlyError as err:
746         logging.error("   Finished with error: {}".
747                       format(str(err).replace("\n", " ")))
748         return