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