004e65e63b6481f7b2d66ea929b8377a0e5dbd81
[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.debug(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.debug(suite)
123                 except KeyError as err:
124                     logging.error("Not found: {0}".format(repr(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     nr_of_samples = list()
133     for val in y_sorted.values():
134         if len(val) > max_len:
135             max_len = len(val)
136         nr_of_samples.append(len(val))
137     for key, val in y_sorted.items():
138         if len(val) < max_len:
139             val.extend([None for _ in range(max_len - len(val))])
140
141     # Add plot traces
142     traces = list()
143     df = pd.DataFrame(y_sorted)
144     df.head()
145     y_max = list()
146     for i, col in enumerate(df.columns):
147         name = "{nr}. ({samples:02d} run{plural}) {name}".\
148             format(nr=(i + 1),
149                    samples=nr_of_samples[i],
150                    plural='s' if nr_of_samples[i] > 1 else '',
151                    name=col.lower().replace('-ndrpdr', ''))
152         if len(name) > 50:
153             name_lst = name.split('-')
154             name = ""
155             split_name = True
156             for segment in name_lst:
157                 if (len(name) + len(segment) + 1) > 50 and split_name:
158                     name += "<br>    "
159                     split_name = False
160                 name += segment + '-'
161             name = name[:-1]
162
163         logging.debug(name)
164         traces.append(plgo.Box(x=[str(i + 1) + '.'] * len(df[col]),
165                                y=[y / 1000000 if y else None for y in df[col]],
166                                name=name,
167                                **plot["traces"]))
168         try:
169             val_max = max(df[col])
170         except ValueError as err:
171             logging.error(repr(err))
172             continue
173         if val_max:
174             y_max.append(int(val_max / 1000000) + 1)
175
176     try:
177         # Create plot
178         layout = deepcopy(plot["layout"])
179         if layout.get("title", None):
180             layout["title"] = "<b>Packet Throughput:</b> {0}". \
181                 format(layout["title"])
182         if y_max:
183             layout["yaxis"]["range"] = [0, max(y_max)]
184         plpl = plgo.Figure(data=traces, layout=layout)
185
186         # Export Plot
187         logging.info("    Writing file '{0}{1}'.".
188                      format(plot["output-file"], plot["output-file-type"]))
189         ploff.plot(plpl, show_link=False, auto_open=False,
190                    filename='{0}{1}'.format(plot["output-file"],
191                                             plot["output-file-type"]))
192     except PlotlyError as err:
193         logging.error("   Finished with error: {}".
194                       format(repr(err).replace("\n", " ")))
195         return
196
197
198 def plot_latency_error_bars(plot, input_data):
199     """Generate the plot(s) with algorithm: plot_latency_error_bars
200     specified in the specification file.
201
202     :param plot: Plot to generate.
203     :param input_data: Data to process.
204     :type plot: pandas.Series
205     :type input_data: InputData
206     """
207
208     # Transform the data
209     plot_title = plot.get("title", "")
210     logging.info("    Creating the data set for the {0} '{1}'.".
211                  format(plot.get("type", ""), plot_title))
212     data = input_data.filter_data(plot)
213     if data is None:
214         logging.error("No data.")
215         return
216
217     # Prepare the data for the plot
218     y_tmp_vals = dict()
219     y_tags = dict()
220     for job in data:
221         for build in job:
222             for test in build:
223                 try:
224                     logging.debug("test['latency']: {0}\n".
225                                  format(test["latency"]))
226                 except ValueError as err:
227                     logging.warning(repr(err))
228                 if y_tmp_vals.get(test["parent"], None) is None:
229                     y_tmp_vals[test["parent"]] = [
230                         list(),  # direction1, min
231                         list(),  # direction1, avg
232                         list(),  # direction1, max
233                         list(),  # direction2, min
234                         list(),  # direction2, avg
235                         list()   # direction2, max
236                     ]
237                     y_tags[test["parent"]] = test.get("tags", None)
238                 try:
239                     if test["type"] in ("NDRPDR", ):
240                         if "-pdr" in plot_title.lower():
241                             ttype = "PDR"
242                         elif "-ndr" in plot_title.lower():
243                             ttype = "NDR"
244                         else:
245                             logging.warning("Invalid test type: {0}".
246                                             format(test["type"]))
247                             continue
248                         y_tmp_vals[test["parent"]][0].append(
249                             test["latency"][ttype]["direction1"]["min"])
250                         y_tmp_vals[test["parent"]][1].append(
251                             test["latency"][ttype]["direction1"]["avg"])
252                         y_tmp_vals[test["parent"]][2].append(
253                             test["latency"][ttype]["direction1"]["max"])
254                         y_tmp_vals[test["parent"]][3].append(
255                             test["latency"][ttype]["direction2"]["min"])
256                         y_tmp_vals[test["parent"]][4].append(
257                             test["latency"][ttype]["direction2"]["avg"])
258                         y_tmp_vals[test["parent"]][5].append(
259                             test["latency"][ttype]["direction2"]["max"])
260                     else:
261                         logging.warning("Invalid test type: {0}".
262                                         format(test["type"]))
263                         continue
264                 except (KeyError, TypeError) as err:
265                     logging.warning(repr(err))
266     logging.debug("y_tmp_vals: {0}\n".format(y_tmp_vals))
267
268     # Sort the tests
269     order = plot.get("sort", None)
270     if order and y_tags:
271         y_sorted = OrderedDict()
272         y_tags_l = {s: [t.lower() for t in ts] for s, ts in y_tags.items()}
273         for tag in order:
274             logging.debug(tag)
275             for suite, tags in y_tags_l.items():
276                 if "not " in tag:
277                     tag = tag.split(" ")[-1]
278                     if tag.lower() in tags:
279                         continue
280                 else:
281                     if tag.lower() not in tags:
282                         continue
283                 try:
284                     y_sorted[suite] = y_tmp_vals.pop(suite)
285                     y_tags_l.pop(suite)
286                     logging.debug(suite)
287                 except KeyError as err:
288                     logging.error("Not found: {0}".format(repr(err)))
289                 finally:
290                     break
291     else:
292         y_sorted = y_tmp_vals
293
294     logging.debug("y_sorted: {0}\n".format(y_sorted))
295     x_vals = list()
296     y_vals = list()
297     y_mins = list()
298     y_maxs = list()
299     nr_of_samples = list()
300     for key, val in y_sorted.items():
301         name = "-".join(key.split("-")[1:-1])
302         if len(name) > 50:
303             name_lst = name.split('-')
304             name = ""
305             split_name = True
306             for segment in name_lst:
307                 if (len(name) + len(segment) + 1) > 50 and split_name:
308                     name += "<br>"
309                     split_name = False
310                 name += segment + '-'
311             name = name[:-1]
312         x_vals.append(name)  # dir 1
313         y_vals.append(mean(val[1]) if val[1] else None)
314         y_mins.append(mean(val[0]) if val[0] else None)
315         y_maxs.append(mean(val[2]) if val[2] else None)
316         nr_of_samples.append(len(val[1]) if val[1] else 0)
317         x_vals.append(name)  # dir 2
318         y_vals.append(mean(val[4]) if val[4] else None)
319         y_mins.append(mean(val[3]) if val[3] else None)
320         y_maxs.append(mean(val[5]) if val[5] else None)
321         nr_of_samples.append(len(val[3]) if val[3] else 0)
322
323     logging.debug("x_vals :{0}\n".format(x_vals))
324     logging.debug("y_vals :{0}\n".format(y_vals))
325     logging.debug("y_mins :{0}\n".format(y_mins))
326     logging.debug("y_maxs :{0}\n".format(y_maxs))
327     logging.debug("nr_of_samples :{0}\n".format(nr_of_samples))
328     traces = list()
329     annotations = list()
330
331     for idx in range(len(x_vals)):
332         if not bool(int(idx % 2)):
333             direction = "West-East"
334         else:
335             direction = "East-West"
336         hovertext = ("No. of Runs: {nr}<br>"
337                      "Test: {test}<br>"
338                      "Direction: {dir}<br>".format(test=x_vals[idx],
339                                                    dir=direction,
340                                                    nr=nr_of_samples[idx]))
341         if isinstance(y_maxs[idx], float):
342             hovertext += "Max: {max:.2f}uSec<br>".format(max=y_maxs[idx])
343         if isinstance(y_vals[idx], float):
344             hovertext += "Mean: {avg:.2f}uSec<br>".format(avg=y_vals[idx])
345         if isinstance(y_mins[idx], float):
346             hovertext += "Min: {min:.2f}uSec".format(min=y_mins[idx])
347
348         if isinstance(y_maxs[idx], float) and isinstance(y_vals[idx], float):
349             array = [y_maxs[idx] - y_vals[idx], ]
350         else:
351             array = [None, ]
352         if isinstance(y_mins[idx], float) and isinstance(y_vals[idx], float):
353             arrayminus = [y_vals[idx] - y_mins[idx], ]
354         else:
355             arrayminus = [None, ]
356         logging.debug("y_vals[{1}] :{0}\n".format(y_vals[idx], idx))
357         logging.debug("array :{0}\n".format(array))
358         logging.debug("arrayminus :{0}\n".format(arrayminus))
359         traces.append(plgo.Scatter(
360             x=[idx, ],
361             y=[y_vals[idx], ],
362             name=x_vals[idx],
363             legendgroup=x_vals[idx],
364             showlegend=bool(int(idx % 2)),
365             mode="markers",
366             error_y=dict(
367                 type='data',
368                 symmetric=False,
369                 array=array,
370                 arrayminus=arrayminus,
371                 color=COLORS[int(idx / 2)]
372             ),
373             marker=dict(
374                 size=10,
375                 color=COLORS[int(idx / 2)],
376             ),
377             text=hovertext,
378             hoverinfo="text",
379         ))
380         annotations.append(dict(
381             x=idx,
382             y=0,
383             xref="x",
384             yref="y",
385             xanchor="center",
386             yanchor="top",
387             text="E-W" if bool(int(idx % 2)) else "W-E",
388             font=dict(
389                 size=16,
390             ),
391             align="center",
392             showarrow=False
393         ))
394
395     try:
396         # Create plot
397         logging.info("    Writing file '{0}{1}'.".
398                      format(plot["output-file"], plot["output-file-type"]))
399         layout = deepcopy(plot["layout"])
400         if layout.get("title", None):
401             layout["title"] = "<b>Packet Latency:</b> {0}".\
402                 format(layout["title"])
403         layout["annotations"] = annotations
404         plpl = plgo.Figure(data=traces, layout=layout)
405
406         # Export Plot
407         ploff.plot(plpl,
408                    show_link=False, auto_open=False,
409                    filename='{0}{1}'.format(plot["output-file"],
410                                             plot["output-file-type"]))
411     except PlotlyError as err:
412         logging.error("   Finished with error: {}".
413                       format(str(err).replace("\n", " ")))
414         return
415
416
417 def plot_throughput_speedup_analysis(plot, input_data):
418     """Generate the plot(s) with algorithm:
419     plot_throughput_speedup_analysis
420     specified in the specification file.
421
422     :param plot: Plot to generate.
423     :param input_data: Data to process.
424     :type plot: pandas.Series
425     :type input_data: InputData
426     """
427
428     # Transform the data
429     plot_title = plot.get("title", "")
430     logging.info("    Creating the data set for the {0} '{1}'.".
431                  format(plot.get("type", ""), plot_title))
432     data = input_data.filter_data(plot)
433     if data is None:
434         logging.error("No data.")
435         return
436
437     y_vals = dict()
438     y_tags = dict()
439     for job in data:
440         for build in job:
441             for test in build:
442                 if y_vals.get(test["parent"], None) is None:
443                     y_vals[test["parent"]] = {"1": list(),
444                                               "2": list(),
445                                               "4": list()}
446                     y_tags[test["parent"]] = test.get("tags", None)
447                 try:
448                     if test["type"] in ("NDRPDR",):
449                         if "-pdr" in plot_title.lower():
450                             ttype = "PDR"
451                         elif "-ndr" in plot_title.lower():
452                             ttype = "NDR"
453                         else:
454                             continue
455                         if "1C" in test["tags"]:
456                             y_vals[test["parent"]]["1"]. \
457                                 append(test["throughput"][ttype]["LOWER"])
458                         elif "2C" in test["tags"]:
459                             y_vals[test["parent"]]["2"]. \
460                                 append(test["throughput"][ttype]["LOWER"])
461                         elif "4C" in test["tags"]:
462                             y_vals[test["parent"]]["4"]. \
463                                 append(test["throughput"][ttype]["LOWER"])
464                 except (KeyError, TypeError):
465                     pass
466
467     if not y_vals:
468         logging.warning("No data for the plot '{}'".
469                         format(plot.get("title", "")))
470         return
471
472     y_1c_max = dict()
473     for test_name, test_vals in y_vals.items():
474         for key, test_val in test_vals.items():
475             if test_val:
476                 avg_val = sum(test_val) / len(test_val)
477                 y_vals[test_name][key] = (avg_val, len(test_val))
478                 ideal = avg_val / (int(key) * 1000000.0)
479                 if test_name not in y_1c_max or ideal > y_1c_max[test_name]:
480                     y_1c_max[test_name] = ideal
481
482     vals = dict()
483     y_max = list()
484     nic_limit = 0
485     lnk_limit = 0
486     pci_limit = plot["limits"]["pci"]["pci-g3-x8"]
487     for test_name, test_vals in y_vals.items():
488         try:
489             if test_vals["1"][1]:
490                 name = "-".join(test_name.split('-')[1:-1])
491                 if len(name) > 50:
492                     name_lst = name.split('-')
493                     name = ""
494                     split_name = True
495                     for segment in name_lst:
496                         if (len(name) + len(segment) + 1) > 50 and split_name:
497                             name += "<br>"
498                             split_name = False
499                         name += segment + '-'
500                     name = name[:-1]
501
502                 vals[name] = dict()
503                 y_val_1 = test_vals["1"][0] / 1000000.0
504                 y_val_2 = test_vals["2"][0] / 1000000.0 if test_vals["2"][0] \
505                     else None
506                 y_val_4 = test_vals["4"][0] / 1000000.0 if test_vals["4"][0] \
507                     else None
508
509                 vals[name]["val"] = [y_val_1, y_val_2, y_val_4]
510                 vals[name]["rel"] = [1.0, None, None]
511                 vals[name]["ideal"] = [y_1c_max[test_name],
512                                        y_1c_max[test_name] * 2,
513                                        y_1c_max[test_name] * 4]
514                 vals[name]["diff"] = [(y_val_1 - y_1c_max[test_name]) * 100 /
515                                       y_val_1, None, None]
516                 vals[name]["count"] = [test_vals["1"][1],
517                                        test_vals["2"][1],
518                                        test_vals["4"][1]]
519
520                 try:
521                     val_max = max(max(vals[name]["val"], vals[name]["ideal"]))
522                 except ValueError as err:
523                     logging.error(err)
524                     continue
525                 if val_max:
526                     y_max.append(int((val_max / 10) + 1) * 10)
527
528                 if y_val_2:
529                     vals[name]["rel"][1] = round(y_val_2 / y_val_1, 2)
530                     vals[name]["diff"][1] = \
531                         (y_val_2 - vals[name]["ideal"][1]) * 100 / y_val_2
532                 if y_val_4:
533                     vals[name]["rel"][2] = round(y_val_4 / y_val_1, 2)
534                     vals[name]["diff"][2] = \
535                         (y_val_4 - vals[name]["ideal"][2]) * 100 / y_val_4
536         except IndexError as err:
537             logging.warning("No data for '{0}'".format(test_name))
538             logging.warning(repr(err))
539
540         # Limits:
541         if "x520" in test_name:
542             limit = plot["limits"]["nic"]["x520"]
543         elif "x710" in test_name:
544             limit = plot["limits"]["nic"]["x710"]
545         elif "xxv710" in test_name:
546             limit = plot["limits"]["nic"]["xxv710"]
547         elif "xl710" in test_name:
548             limit = plot["limits"]["nic"]["xl710"]
549         else:
550             limit = 0
551         if limit > nic_limit:
552             nic_limit = limit
553
554         mul = 2 if "ge2p" in test_name else 1
555         if "10ge" in test_name:
556             limit = plot["limits"]["link"]["10ge"] * mul
557         elif "25ge" in test_name:
558             limit = plot["limits"]["link"]["25ge"] * mul
559         elif "40ge" in test_name:
560             limit = plot["limits"]["link"]["40ge"] * mul
561         elif "100ge" in test_name:
562             limit = plot["limits"]["link"]["100ge"] * mul
563         else:
564             limit = 0
565         if limit > lnk_limit:
566             lnk_limit = limit
567
568     # Sort the tests
569     order = plot.get("sort", None)
570     if order and y_tags:
571         y_sorted = OrderedDict()
572         y_tags_l = {s: [t.lower() for t in ts] for s, ts in y_tags.items()}
573         for tag in order:
574             for test, tags in y_tags_l.items():
575                 if tag.lower() in tags:
576                     name = "-".join(test.split('-')[1:-1])
577                     try:
578                         y_sorted[name] = vals.pop(name)
579                         y_tags_l.pop(test)
580                     except KeyError as err:
581                         logging.error("Not found: {0}".format(err))
582                     finally:
583                         break
584     else:
585         y_sorted = vals
586
587     traces = list()
588     annotations = list()
589     x_vals = [1, 2, 4]
590
591     # Limits:
592     try:
593         threshold = 1.1 * max(y_max)  # 10%
594     except ValueError as err:
595         logging.error(err)
596         return
597     nic_limit /= 1000000.0
598     if nic_limit < threshold:
599         traces.append(plgo.Scatter(
600             x=x_vals,
601             y=[nic_limit, ] * len(x_vals),
602             name="NIC: {0:.2f}Mpps".format(nic_limit),
603             showlegend=False,
604             mode="lines",
605             line=dict(
606                 dash="dot",
607                 color=COLORS[-1],
608                 width=1),
609             hoverinfo="none"
610         ))
611         annotations.append(dict(
612             x=1,
613             y=nic_limit,
614             xref="x",
615             yref="y",
616             xanchor="left",
617             yanchor="bottom",
618             text="NIC: {0:.2f}Mpps".format(nic_limit),
619             font=dict(
620                 size=14,
621                 color=COLORS[-1],
622             ),
623             align="left",
624             showarrow=False
625         ))
626         y_max.append(int((nic_limit / 10) + 1) * 10)
627
628     lnk_limit /= 1000000.0
629     if lnk_limit < threshold:
630         traces.append(plgo.Scatter(
631             x=x_vals,
632             y=[lnk_limit, ] * len(x_vals),
633             name="Link: {0:.2f}Mpps".format(lnk_limit),
634             showlegend=False,
635             mode="lines",
636             line=dict(
637                 dash="dot",
638                 color=COLORS[-2],
639                 width=1),
640             hoverinfo="none"
641         ))
642         annotations.append(dict(
643             x=1,
644             y=lnk_limit,
645             xref="x",
646             yref="y",
647             xanchor="left",
648             yanchor="bottom",
649             text="Link: {0:.2f}Mpps".format(lnk_limit),
650             font=dict(
651                 size=14,
652                 color=COLORS[-2],
653             ),
654             align="left",
655             showarrow=False
656         ))
657         y_max.append(int((lnk_limit / 10) + 1) * 10)
658
659     pci_limit /= 1000000.0
660     if pci_limit < threshold:
661         traces.append(plgo.Scatter(
662             x=x_vals,
663             y=[pci_limit, ] * len(x_vals),
664             name="PCIe: {0:.2f}Mpps".format(pci_limit),
665             showlegend=False,
666             mode="lines",
667             line=dict(
668                 dash="dot",
669                 color=COLORS[-3],
670                 width=1),
671             hoverinfo="none"
672         ))
673         annotations.append(dict(
674             x=1,
675             y=pci_limit,
676             xref="x",
677             yref="y",
678             xanchor="left",
679             yanchor="bottom",
680             text="PCIe: {0:.2f}Mpps".format(pci_limit),
681             font=dict(
682                 size=14,
683                 color=COLORS[-3],
684             ),
685             align="left",
686             showarrow=False
687         ))
688         y_max.append(int((pci_limit / 10) + 1) * 10)
689
690     # Perfect and measured:
691     cidx = 0
692     for name, val in y_sorted.iteritems():
693         hovertext = list()
694         try:
695             for idx in range(len(val["val"])):
696                 htext = ""
697                 if isinstance(val["val"][idx], float):
698                     htext += "No. of Runs: {1}<br>" \
699                              "Mean: {0:.2f}Mpps<br>".format(val["val"][idx],
700                                                             val["count"][idx])
701                 if isinstance(val["diff"][idx], float):
702                     htext += "Diff: {0:.0f}%<br>".format(round(val["diff"][idx]))
703                 if isinstance(val["rel"][idx], float):
704                     htext += "Speedup: {0:.2f}".format(val["rel"][idx])
705                 hovertext.append(htext)
706             traces.append(plgo.Scatter(x=x_vals,
707                                        y=val["val"],
708                                        name=name,
709                                        legendgroup=name,
710                                        mode="lines+markers",
711                                        line=dict(
712                                            color=COLORS[cidx],
713                                            width=2),
714                                        marker=dict(
715                                            symbol="circle",
716                                            size=10
717                                        ),
718                                        text=hovertext,
719                                        hoverinfo="text+name"
720                                        ))
721             traces.append(plgo.Scatter(x=x_vals,
722                                        y=val["ideal"],
723                                        name="{0} perfect".format(name),
724                                        legendgroup=name,
725                                        showlegend=False,
726                                        mode="lines",
727                                        line=dict(
728                                            color=COLORS[cidx],
729                                            width=2,
730                                            dash="dash"),
731                                        text=["Perfect: {0:.2f}Mpps".format(y)
732                                              for y in val["ideal"]],
733                                        hoverinfo="text"
734                                        ))
735             cidx += 1
736         except (IndexError, ValueError, KeyError) as err:
737             logging.warning("No data for '{0}'".format(name))
738             logging.warning(repr(err))
739
740     try:
741         # Create plot
742         logging.info("    Writing file '{0}{1}'.".
743                      format(plot["output-file"], plot["output-file-type"]))
744         layout = deepcopy(plot["layout"])
745         if layout.get("title", None):
746             layout["title"] = "<b>Speedup Multi-core:</b> {0}". \
747                 format(layout["title"])
748         layout["annotations"].extend(annotations)
749         plpl = plgo.Figure(data=traces, layout=layout)
750
751         # Export Plot
752         ploff.plot(plpl,
753                    show_link=False, auto_open=False,
754                    filename='{0}{1}'.format(plot["output-file"],
755                                             plot["output-file-type"]))
756     except PlotlyError as err:
757         logging.error("   Finished with error: {}".
758                       format(str(err).replace("\n", " ")))
759         return
760
761
762 def plot_http_server_performance_box(plot, input_data):
763     """Generate the plot(s) with algorithm: plot_http_server_performance_box
764     specified in the specification file.
765
766     :param plot: Plot to generate.
767     :param input_data: Data to process.
768     :type plot: pandas.Series
769     :type input_data: InputData
770     """
771
772     # Transform the data
773     logging.info("    Creating the data set for the {0} '{1}'.".
774                  format(plot.get("type", ""), plot.get("title", "")))
775     data = input_data.filter_data(plot)
776     if data is None:
777         logging.error("No data.")
778         return
779
780     # Prepare the data for the plot
781     y_vals = dict()
782     for job in data:
783         for build in job:
784             for test in build:
785                 if y_vals.get(test["name"], None) is None:
786                     y_vals[test["name"]] = list()
787                 try:
788                     y_vals[test["name"]].append(test["result"])
789                 except (KeyError, TypeError):
790                     y_vals[test["name"]].append(None)
791
792     # Add None to the lists with missing data
793     max_len = 0
794     nr_of_samples = list()
795     for val in y_vals.values():
796         if len(val) > max_len:
797             max_len = len(val)
798         nr_of_samples.append(len(val))
799     for key, val in y_vals.items():
800         if len(val) < max_len:
801             val.extend([None for _ in range(max_len - len(val))])
802
803     # Add plot traces
804     traces = list()
805     df = pd.DataFrame(y_vals)
806     df.head()
807     for i, col in enumerate(df.columns):
808         name = "{nr}. ({samples:02d} run{plural}) {name}".\
809             format(nr=(i + 1),
810                    samples=nr_of_samples[i],
811                    plural='s' if nr_of_samples[i] > 1 else '',
812                    name=col.lower().replace('-ndrpdr', ''))
813         if len(name) > 50:
814             name_lst = name.split('-')
815             name = ""
816             split_name = True
817             for segment in name_lst:
818                 if (len(name) + len(segment) + 1) > 50 and split_name:
819                     name += "<br>    "
820                     split_name = False
821                 name += segment + '-'
822             name = name[:-1]
823
824         traces.append(plgo.Box(x=[str(i + 1) + '.'] * len(df[col]),
825                                y=df[col],
826                                name=name,
827                                **plot["traces"]))
828     try:
829         # Create plot
830         plpl = plgo.Figure(data=traces, layout=plot["layout"])
831
832         # Export Plot
833         logging.info("    Writing file '{0}{1}'.".
834                      format(plot["output-file"], plot["output-file-type"]))
835         ploff.plot(plpl, show_link=False, auto_open=False,
836                    filename='{0}{1}'.format(plot["output-file"],
837                                             plot["output-file-type"]))
838     except PlotlyError as err:
839         logging.error("   Finished with error: {}".
840                       format(str(err).replace("\n", " ")))
841         return