C-Dash: Add detailed views to comparison tables
[csit.git] / csit.infra.dash / app / cdash / utils / utils.py
1 # Copyright (c) 2024 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 """Functions used by Dash applications.
15 """
16
17 import pandas as pd
18 import plotly.graph_objects as go
19 import dash_bootstrap_components as dbc
20
21 import hdrh.histogram
22 import hdrh.codec
23
24 from math import sqrt
25 from dash import dcc, no_update, html
26 from datetime import datetime
27
28 from ..utils.constants import Constants as C
29 from ..utils.url_processing import url_encode
30 from ..utils.trigger import Trigger
31
32
33 def get_color(idx: int) -> str:
34     """Returns a color from the list defined in Constants.PLOT_COLORS defined by
35     its index.
36
37     :param idx: Index of the color.
38     :type idx: int
39     :returns: Color defined by hex code.
40     :trype: str
41     """
42     return C.PLOT_COLORS[idx % len(C.PLOT_COLORS)]
43
44
45 def show_tooltip(tooltips:dict, id: str, title: str,
46         clipboard_id: str=None) -> list:
47     """Generate list of elements to display a text (e.g. a title) with a
48     tooltip and optionaly with Copy&Paste icon and the clipboard
49     functionality enabled.
50
51     :param tooltips: Dictionary with tooltips.
52     :param id: Tooltip ID.
53     :param title: A text for which the tooltip will be displayed.
54     :param clipboard_id: If defined, a Copy&Paste icon is displayed and the
55         clipboard functionality is enabled.
56     :type tooltips: dict
57     :type id: str
58     :type title: str
59     :type clipboard_id: str
60     :returns: List of elements to display a text with a tooltip and
61         optionaly with Copy&Paste icon.
62     :rtype: list
63     """
64
65     return [
66         dcc.Clipboard(target_id=clipboard_id, title="Copy URL") \
67             if clipboard_id else str(),
68         f"{title} ",
69         dbc.Badge(
70             id=id,
71             children="?",
72             pill=True,
73             color="white",
74             text_color="info",
75             class_name="border ms-1",
76         ),
77         dbc.Tooltip(
78             children=tooltips.get(id, str()),
79             target=id,
80             placement="auto"
81         )
82     ]
83
84
85 def label(key: str) -> str:
86     """Returns a label for input elements (dropdowns, ...).
87
88     If the label is not defined, the function returns the provided key.
89
90     :param key: The key to the label defined in Constants.LABELS.
91     :type key: str
92     :returns: Label.
93     :rtype: str
94     """
95     return C.LABELS.get(key, key)
96
97
98 def sync_checklists(options: list, sel: list, all: list, id: str) -> tuple:
99     """Synchronize a checklist with defined "options" with its "All" checklist.
100
101     :param options: List of options for the cheklist.
102     :param sel: List of selected options.
103     :param all: List of selected option from "All" checklist.
104     :param id: ID of a checklist to be used for synchronization.
105     :returns: Tuple of lists with otions for both checklists.
106     :rtype: tuple of lists
107     """
108     opts = {v["value"] for v in options}
109     if id =="all":
110         sel = list(opts) if all else list()
111     else:
112         all = ["all", ] if set(sel) == opts else list()
113     return sel, all
114
115
116 def list_tests(selection: dict) -> list:
117     """Transform list of tests to a list of dictionaries usable by checkboxes.
118
119     :param selection: List of tests to be displayed in "Selected tests" window.
120     :type selection: list
121     :returns: List of dictionaries with "label", "value" pairs for a checkbox.
122     :rtype: list
123     """
124     if selection:
125         return [{"label": v["id"], "value": v["id"]} for v in selection]
126     else:
127         return list()
128
129
130 def get_date(s_date: str) -> datetime:
131     """Transform string reprezentation of date to datetime.datetime data type.
132
133     :param s_date: String reprezentation of date.
134     :type s_date: str
135     :returns: Date as datetime.datetime.
136     :rtype: datetime.datetime
137     """
138     return datetime(int(s_date[0:4]), int(s_date[5:7]), int(s_date[8:10]))
139
140
141 def gen_new_url(url_components: dict, params: dict) -> str:
142     """Generate a new URL with encoded parameters.
143
144     :param url_components: Dictionary with URL elements. It should contain
145         "scheme", "netloc" and "path".
146     :param url_components: URL parameters to be encoded to the URL.
147     :type parsed_url: dict
148     :type params: dict
149     :returns Encoded URL with parameters.
150     :rtype: str
151     """
152
153     if url_components:
154         return url_encode(
155             {
156                 "scheme": url_components.get("scheme", ""),
157                 "netloc": url_components.get("netloc", ""),
158                 "path": url_components.get("path", ""),
159                 "params": params
160             }
161         )
162     else:
163         return str()
164
165
166 def get_duts(df: pd.DataFrame) -> list:
167     """Get the list of DUTs from the pre-processed information about jobs.
168
169     :param df: DataFrame with information about jobs.
170     :type df: pandas.DataFrame
171     :returns: Alphabeticaly sorted list of DUTs.
172     :rtype: list
173     """
174     return sorted(list(df["dut"].unique()))
175
176
177 def get_ttypes(df: pd.DataFrame, dut: str) -> list:
178     """Get the list of test types from the pre-processed information about
179     jobs.
180
181     :param df: DataFrame with information about jobs.
182     :param dut: The DUT for which the list of test types will be populated.
183     :type df: pandas.DataFrame
184     :type dut: str
185     :returns: Alphabeticaly sorted list of test types.
186     :rtype: list
187     """
188     return sorted(list(df.loc[(df["dut"] == dut)]["ttype"].unique()))
189
190
191 def get_cadences(df: pd.DataFrame, dut: str, ttype: str) -> list:
192     """Get the list of cadences from the pre-processed information about
193     jobs.
194
195     :param df: DataFrame with information about jobs.
196     :param dut: The DUT for which the list of cadences will be populated.
197     :param ttype: The test type for which the list of cadences will be
198         populated.
199     :type df: pandas.DataFrame
200     :type dut: str
201     :type ttype: str
202     :returns: Alphabeticaly sorted list of cadences.
203     :rtype: list
204     """
205     return sorted(list(df.loc[(
206         (df["dut"] == dut) &
207         (df["ttype"] == ttype)
208     )]["cadence"].unique()))
209
210
211 def get_test_beds(df: pd.DataFrame, dut: str, ttype: str, cadence: str) -> list:
212     """Get the list of test beds from the pre-processed information about
213     jobs.
214
215     :param df: DataFrame with information about jobs.
216     :param dut: The DUT for which the list of test beds will be populated.
217     :param ttype: The test type for which the list of test beds will be
218         populated.
219     :param cadence: The cadence for which the list of test beds will be
220         populated.
221     :type df: pandas.DataFrame
222     :type dut: str
223     :type ttype: str
224     :type cadence: str
225     :returns: Alphabeticaly sorted list of test beds.
226     :rtype: list
227     """
228     return sorted(list(df.loc[(
229         (df["dut"] == dut) &
230         (df["ttype"] == ttype) &
231         (df["cadence"] == cadence)
232     )]["tbed"].unique()))
233
234
235 def get_job(df: pd.DataFrame, dut, ttype, cadence, testbed):
236     """Get the name of a job defined by dut, ttype, cadence, test bed.
237     Input information comes from the control panel.
238
239     :param df: DataFrame with information about jobs.
240     :param dut: The DUT for which the job name will be created.
241     :param ttype: The test type for which the job name will be created.
242     :param cadence: The cadence for which the job name will be created.
243     :param testbed: The test bed for which the job name will be created.
244     :type df: pandas.DataFrame
245     :type dut: str
246     :type ttype: str
247     :type cadence: str
248     :type testbed: str
249     :returns: Job name.
250     :rtype: str
251     """
252     return df.loc[(
253         (df["dut"] == dut) &
254         (df["ttype"] == ttype) &
255         (df["cadence"] == cadence) &
256         (df["tbed"] == testbed)
257     )]["job"].item()
258
259
260 def generate_options(opts: list, sort: bool=True) -> list:
261     """Return list of options for radio items in control panel. The items in
262     the list are dictionaries with keys "label" and "value".
263
264     :params opts: List of options (str) to be used for the generated list.
265     :type opts: list
266     :returns: List of options (dict).
267     :rtype: list
268     """
269     if sort:
270         opts = sorted(opts)
271     return [{"label": i, "value": i} for i in opts]
272
273
274 def set_job_params(df: pd.DataFrame, job: str) -> dict:
275     """Create a dictionary with all options and values for (and from) the
276     given job.
277
278     :param df: DataFrame with information about jobs.
279     :params job: The name of job for and from which the dictionary will be
280         created.
281     :type df: pandas.DataFrame
282     :type job: str
283     :returns: Dictionary with all options and values for (and from) the
284         given job.
285     :rtype: dict
286     """
287
288     l_job = job.split("-")
289     return {
290         "job": job,
291         "dut": l_job[1],
292         "ttype": l_job[3],
293         "cadence": l_job[4],
294         "tbed": "-".join(l_job[-2:]),
295         "duts": generate_options(get_duts(df)),
296         "ttypes": generate_options(get_ttypes(df, l_job[1])),
297         "cadences": generate_options(get_cadences(df, l_job[1], l_job[3])),
298         "tbeds": generate_options(
299             get_test_beds(df, l_job[1], l_job[3], l_job[4]))
300     }
301
302
303 def get_list_group_items(
304         items: list,
305         type: str,
306         colorize: bool=True,
307         add_index: bool=False
308     ) -> list:
309     """Generate list of ListGroupItems with checkboxes with selected items.
310
311     :param items: List of items to be displayed in the ListGroup.
312     :param type: The type part of an element ID.
313     :param colorize: If True, the color of labels is set, otherwise the default
314         color is used.
315     :param add_index: Add index to the list items.
316     :type items: list
317     :type type: str
318     :type colorize: bool
319     :type add_index: bool
320     :returns: List of ListGroupItems with checkboxes with selected items.
321     :rtype: list
322     """
323
324     children = list()
325     for i, l in enumerate(items):
326         idx = f"{i + 1}. " if add_index else str()
327         label = f"{idx}{l['id']}" if isinstance(l, dict) else f"{idx}{l}"
328         children.append(
329             dbc.ListGroupItem(
330                 children=[
331                     dbc.Checkbox(
332                         id={"type": type, "index": i},
333                         label=label,
334                         value=False,
335                         label_class_name="m-0 p-0",
336                         label_style={
337                             "font-size": ".875em",
338                             "color": get_color(i) if colorize else "#55595c"
339                         },
340                         class_name="info"
341                     )
342                 ],
343                 class_name="p-0"
344             )
345         )
346
347     return children
348
349
350 def relative_change_stdev(mean1, mean2, std1, std2):
351     """Compute relative standard deviation of change of two values.
352
353     The "1" values are the base for comparison.
354     Results are returned as percentage (and percentual points for stdev).
355     Linearized theory is used, so results are wrong for relatively large stdev.
356
357     :param mean1: Mean of the first number.
358     :param mean2: Mean of the second number.
359     :param std1: Standard deviation estimate of the first number.
360     :param std2: Standard deviation estimate of the second number.
361     :type mean1: float
362     :type mean2: float
363     :type std1: float
364     :type std2: float
365     :returns: Relative change and its stdev.
366     :rtype: float
367     """
368     mean1, mean2 = float(mean1), float(mean2)
369     quotient = mean2 / mean1
370     first = std1 / mean1
371     second = std2 / mean2
372     std = quotient * sqrt(first * first + second * second)
373     return (quotient - 1) * 100, std * 100
374
375
376 def get_hdrh_latencies(row: pd.Series, name: str) -> dict:
377     """Get the HDRH latencies from the test data.
378
379     :param row: A row fron the data frame with test data.
380     :param name: The test name to be displayed as the graph title.
381     :type row: pandas.Series
382     :type name: str
383     :returns: Dictionary with HDRH latencies.
384     :rtype: dict
385     """
386
387     latencies = {"name": name}
388     for key in C.LAT_HDRH:
389         try:
390             latencies[key] = row[key]
391         except KeyError:
392             return None
393
394     return latencies
395
396
397 def graph_hdrh_latency(data: dict, layout: dict) -> go.Figure:
398     """Generate HDR Latency histogram graphs.
399
400     :param data: HDRH data.
401     :param layout: Layout of plot.ly graph.
402     :type data: dict
403     :type layout: dict
404     :returns: HDR latency Histogram.
405     :rtype: plotly.graph_objects.Figure
406     """
407
408     fig = None
409
410     traces = list()
411     for idx, (lat_name, lat_hdrh) in enumerate(data.items()):
412         try:
413             decoded = hdrh.histogram.HdrHistogram.decode(lat_hdrh)
414         except (hdrh.codec.HdrLengthException, TypeError):
415             continue
416         previous_x = 0.0
417         prev_perc = 0.0
418         xaxis = list()
419         yaxis = list()
420         hovertext = list()
421         for item in decoded.get_recorded_iterator():
422             # The real value is "percentile".
423             # For 100%, we cut that down to "x_perc" to avoid
424             # infinity.
425             percentile = item.percentile_level_iterated_to
426             x_perc = min(percentile, C.PERCENTILE_MAX)
427             xaxis.append(previous_x)
428             yaxis.append(item.value_iterated_to)
429             hovertext.append(
430                 f"<b>{C.GRAPH_LAT_HDRH_DESC[lat_name]}</b><br>"
431                 f"Direction: {('W-E', 'E-W')[idx % 2]}<br>"
432                 f"Percentile: {prev_perc:.5f}-{percentile:.5f}%<br>"
433                 f"Latency: {item.value_iterated_to}uSec"
434             )
435             next_x = 100.0 / (100.0 - x_perc)
436             xaxis.append(next_x)
437             yaxis.append(item.value_iterated_to)
438             hovertext.append(
439                 f"<b>{C.GRAPH_LAT_HDRH_DESC[lat_name]}</b><br>"
440                 f"Direction: {('W-E', 'E-W')[idx % 2]}<br>"
441                 f"Percentile: {prev_perc:.5f}-{percentile:.5f}%<br>"
442                 f"Latency: {item.value_iterated_to}uSec"
443             )
444             previous_x = next_x
445             prev_perc = percentile
446
447         traces.append(
448             go.Scatter(
449                 x=xaxis,
450                 y=yaxis,
451                 name=C.GRAPH_LAT_HDRH_DESC[lat_name],
452                 mode="lines",
453                 legendgroup=C.GRAPH_LAT_HDRH_DESC[lat_name],
454                 showlegend=bool(idx % 2),
455                 line=dict(
456                     color=get_color(int(idx/2)),
457                     dash="solid",
458                     width=1 if idx % 2 else 2
459                 ),
460                 hovertext=hovertext,
461                 hoverinfo="text"
462             )
463         )
464     if traces:
465         fig = go.Figure()
466         fig.add_traces(traces)
467         layout_hdrh = layout.get("plot-hdrh-latency", None)
468         if lat_hdrh:
469             fig.update_layout(layout_hdrh)
470
471     return fig
472
473
474 def navbar_trending(active: tuple):
475     """Add nav element with navigation panel. It is placed on the top.
476
477     :param active: Tuple of boolean values defining the active items in the
478         navbar. True == active
479     :type active: tuple
480     :returns: Navigation bar.
481     :rtype: dbc.NavbarSimple
482     """
483     return dbc.NavbarSimple(
484         children=[
485             dbc.NavItem(dbc.NavLink(
486                 C.TREND_TITLE,
487                 active=active[0],
488                 external_link=True,
489                 href="/trending"
490             )),
491             dbc.NavItem(dbc.NavLink(
492                 C.NEWS_TITLE,
493                 active=active[1],
494                 external_link=True,
495                 href="/news"
496             )),
497             dbc.NavItem(dbc.NavLink(
498                 C.STATS_TITLE,
499                 active=active[2],
500                 external_link=True,
501                 href="/stats"
502             )),
503             dbc.NavItem(dbc.NavLink(
504                 C.SEARCH_TITLE,
505                 active=active[3],
506                 external_link=True,
507                 href="/search"
508             )),
509             dbc.NavItem(dbc.NavLink(
510                 "Documentation",
511                 id="btn-documentation",
512             ))
513         ],
514         id="navbarsimple-main",
515         brand=C.BRAND,
516         brand_href="/",
517         brand_external_link=True,
518         class_name="p-2",
519         fluid=True
520     )
521
522
523 def navbar_report(active: tuple):
524     """Add nav element with navigation panel. It is placed on the top.
525
526     :param active: Tuple of boolean values defining the active items in the
527         navbar. True == active
528     :type active: tuple
529     :returns: Navigation bar.
530     :rtype: dbc.NavbarSimple
531     """
532     return dbc.NavbarSimple(
533         id="navbarsimple-main",
534         children=[
535             dbc.NavItem(dbc.NavLink(
536                 C.REPORT_TITLE,
537                 active=active[0],
538                 external_link=True,
539                 href="/report"
540             )),
541             dbc.NavItem(dbc.NavLink(
542                 "Comparisons",
543                 active=active[1],
544                 external_link=True,
545                 href="/comparisons"
546             )),
547             dbc.NavItem(dbc.NavLink(
548                 "Coverage Data",
549                 active=active[2],
550                 external_link=True,
551                 href="/coverage"
552             )),
553             dbc.NavItem(dbc.NavLink(
554                 C.SEARCH_TITLE,
555                 active=active[3],
556                 external_link=True,
557                 href="/search"
558             )),
559             dbc.NavItem(dbc.NavLink(
560                 "Documentation",
561                 id="btn-documentation",
562             ))
563         ],
564         brand=C.BRAND,
565         brand_href="/",
566         brand_external_link=True,
567         class_name="p-2",
568         fluid=True
569     )
570
571
572 def filter_table_data(
573         store_table_data: list,
574         table_filter: str
575     ) -> list:
576     """Filter table data using user specified filter.
577
578     :param store_table_data: Table data represented as a list of records.
579     :param table_filter: User specified filter.
580     :type store_table_data: list
581     :type table_filter: str
582     :returns: A new table created by filtering of table data represented as
583         a list of records.
584     :rtype: list
585     """
586
587     # Checks:
588     if not any((table_filter, store_table_data, )):
589         return store_table_data
590
591     def _split_filter_part(filter_part: str) -> tuple:
592         """Split a part of filter into column name, operator and value.
593         A "part of filter" is a sting berween "&&" operator.
594
595         :param filter_part: A part of filter.
596         :type filter_part: str
597         :returns: Column name, operator, value
598         :rtype: tuple[str, str, str|float]
599         """
600         for operator_type in C.OPERATORS:
601             for operator in operator_type:
602                 if operator in filter_part:
603                     name_p, val_p = filter_part.split(operator, 1)
604                     name = name_p[name_p.find("{") + 1 : name_p.rfind("}")]
605                     val_p = val_p.strip()
606                     if (val_p[0] == val_p[-1] and val_p[0] in ("'", '"', '`')):
607                         value = val_p[1:-1].replace("\\" + val_p[0], val_p[0])
608                     else:
609                         try:
610                             value = float(val_p)
611                         except ValueError:
612                             value = val_p
613
614                     return name, operator_type[0].strip(), value
615         return (None, None, None)
616
617     df = pd.DataFrame.from_records(store_table_data)
618     for filter_part in table_filter.split(" && "):
619         col_name, operator, filter_value = _split_filter_part(filter_part)
620         if operator == "contains":
621             df = df.loc[df[col_name].str.contains(filter_value, regex=True)]
622         elif operator in ("eq", "ne", "lt", "le", "gt", "ge"):
623             # These operators match pandas series operator method names.
624             df = df.loc[getattr(df[col_name], operator)(filter_value)]
625         elif operator == "datestartswith":
626             # This is a simplification of the front-end filtering logic,
627             # only works with complete fields in standard format.
628             # Currently not used in comparison tables.
629             df = df.loc[df[col_name].str.startswith(filter_value)]
630
631     return df.to_dict("records")
632
633
634 def sort_table_data(
635         store_table_data: list,
636         sort_by: list
637     ) -> list:
638     """Sort table data using user specified order.
639
640     :param store_table_data: Table data represented as a list of records.
641     :param sort_by: User specified sorting order (multicolumn).
642     :type store_table_data: list
643     :type sort_by: list
644     :returns: A new table created by sorting the table data represented as
645         a list of records.
646     :rtype: list
647     """
648
649     # Checks:
650     if not any((sort_by, store_table_data, )):
651         return store_table_data
652
653     df = pd.DataFrame.from_records(store_table_data)
654     if len(sort_by):
655         dff = df.sort_values(
656             [col["column_id"] for col in sort_by],
657             ascending=[col["direction"] == "asc" for col in sort_by],
658             inplace=False
659         )
660     else:
661         # No sort is applied
662         dff = df
663
664     return dff.to_dict("records")
665
666
667 def show_trending_graph_data(
668         trigger: Trigger,
669         data: dict,
670         graph_layout: dict
671     ) -> tuple:
672     """Generates the data for the offcanvas displayed when a particular point in
673     a trending graph (daily data) is clicked on.
674
675     :param trigger: The information from trigger when the data point is clicked
676         on.
677     :param graph: The data from the clicked point in the graph.
678     :param graph_layout: The layout of the HDRH latency graph.
679     :type trigger: Trigger
680     :type graph: dict
681     :type graph_layout: dict
682     :returns: The data to be displayed on the offcanvas and the information to
683         show the offcanvas.
684     :rtype: tuple(list, list, bool)
685     """
686
687     if trigger.idx == "tput":
688         idx = 0
689     elif trigger.idx == "bandwidth":
690         idx = 1
691     elif trigger.idx == "lat":
692         idx = len(data) - 1
693     else:
694         return list(), list(), False
695     try:
696         data = data[idx]["points"][0]
697     except (IndexError, KeyError, ValueError, TypeError):
698         return list(), list(), False
699
700     metadata = no_update
701     graph = list()
702
703     list_group_items = list()
704     for itm in data.get("text", None).split("<br>"):
705         if not itm:
706             continue
707         lst_itm = itm.split(": ")
708         if lst_itm[0] == "csit-ref":
709             list_group_item = dbc.ListGroupItem([
710                 dbc.Badge(lst_itm[0]),
711                 html.A(
712                     lst_itm[1],
713                     href=f"{C.URL_JENKINS}{lst_itm[1]}",
714                     target="_blank"
715                 )
716             ])
717         else:
718             list_group_item = dbc.ListGroupItem([
719                 dbc.Badge(lst_itm[0]),
720                 lst_itm[1]
721             ])
722         list_group_items.append(list_group_item)
723
724     if trigger.idx == "tput":
725         title = "Throughput"
726     elif trigger.idx == "bandwidth":
727         title = "Bandwidth"
728     elif trigger.idx == "lat":
729         title = "Latency"
730         hdrh_data = data.get("customdata", None)
731         if hdrh_data:
732             graph = [dbc.Card(
733                 class_name="gy-2 p-0",
734                 children=[
735                     dbc.CardHeader(hdrh_data.pop("name")),
736                     dbc.CardBody(
737                         dcc.Graph(
738                             id="hdrh-latency-graph",
739                             figure=graph_hdrh_latency(hdrh_data, graph_layout)
740                         )
741                     )
742                 ])
743             ]
744
745     metadata = [
746         dbc.Card(
747             class_name="gy-2 p-0",
748             children=[
749                 dbc.CardHeader(children=[
750                     dcc.Clipboard(
751                         target_id="tput-lat-metadata",
752                         title="Copy",
753                         style={"display": "inline-block"}
754                     ),
755                     title
756                 ]),
757                 dbc.CardBody(
758                     dbc.ListGroup(list_group_items, flush=True),
759                     id="tput-lat-metadata",
760                     class_name="p-0",
761                 )
762             ]
763         )
764     ]
765
766     return metadata, graph, True
767
768
769 def show_iterative_graph_data(
770         trigger: Trigger,
771         data: dict,
772         graph_layout: dict
773     ) -> tuple:
774     """Generates the data for the offcanvas displayed when a particular point in
775     a box graph (iterative data) is clicked on.
776
777     :param trigger: The information from trigger when the data point is clicked
778         on.
779     :param graph: The data from the clicked point in the graph.
780     :param graph_layout: The layout of the HDRH latency graph.
781     :type trigger: Trigger
782     :type graph: dict
783     :type graph_layout: dict
784     :returns: The data to be displayed on the offcanvas and the information to
785         show the offcanvas.
786     :rtype: tuple(list, list, bool)
787     """
788
789     if trigger.idx == "tput":
790         idx = 0
791     elif trigger.idx == "bandwidth":
792         idx = 1
793     elif trigger.idx == "lat":
794         idx = len(data) - 1
795     else:
796         return list(), list(), False
797
798     try:
799         data = data[idx]["points"]
800     except (IndexError, KeyError, ValueError, TypeError):
801         return list(), list(), False
802
803     def _process_stats(data: list, param: str) -> list:
804         """Process statistical data provided by plot.ly box graph.
805
806         :param data: Statistical data provided by plot.ly box graph.
807         :param param: Parameter saying if the data come from "tput" or
808             "lat" graph.
809         :type data: list
810         :type param: str
811         :returns: Listo of tuples where the first value is the
812             statistic's name and the secont one it's value.
813         :rtype: list
814         """
815         if len(data) == 7:
816             stats = ("max", "upper fence", "q3", "median", "q1",
817                     "lower fence", "min")
818         elif len(data) == 9:
819             stats = ("outlier", "max", "upper fence", "q3", "median",
820                     "q1", "lower fence", "min", "outlier")
821         elif len(data) == 1:
822             if param == "lat":
823                 stats = ("average latency at 50% PDR", )
824             elif param == "bandwidth":
825                 stats = ("bandwidth", )
826             else:
827                 stats = ("throughput", )
828         else:
829             return list()
830         unit = " [us]" if param == "lat" else str()
831         return [(f"{stat}{unit}", f"{value['y']:,.0f}")
832                 for stat, value in zip(stats, data)]
833
834     customdata = data[0].get("customdata", dict())
835     datapoint = customdata.get("metadata", dict())
836     hdrh_data = customdata.get("hdrh", dict())
837
838     list_group_items = list()
839     for k, v in datapoint.items():
840         if k == "csit-ref":
841             if len(data) > 1:
842                 continue
843             list_group_item = dbc.ListGroupItem([
844                 dbc.Badge(k),
845                 html.A(v, href=f"{C.URL_JENKINS}{v}", target="_blank")
846             ])
847         else:
848             list_group_item = dbc.ListGroupItem([dbc.Badge(k), v])
849         list_group_items.append(list_group_item)
850
851     graph = list()
852     if trigger.idx == "tput":
853         title = "Throughput"
854     elif trigger.idx == "bandwidth":
855         title = "Bandwidth"
856     elif trigger.idx == "lat":
857         title = "Latency"
858         if len(data) == 1:
859             if hdrh_data:
860                 graph = [dbc.Card(
861                     class_name="gy-2 p-0",
862                     children=[
863                         dbc.CardHeader(hdrh_data.pop("name")),
864                         dbc.CardBody(dcc.Graph(
865                             id="hdrh-latency-graph",
866                             figure=graph_hdrh_latency(hdrh_data, graph_layout)
867                         ))
868                     ])
869                 ]
870
871     for k, v in _process_stats(data, trigger.idx):
872         list_group_items.append(dbc.ListGroupItem([dbc.Badge(k), v]))
873
874     metadata = [
875         dbc.Card(
876             class_name="gy-2 p-0",
877             children=[
878                 dbc.CardHeader(children=[
879                     dcc.Clipboard(
880                         target_id="tput-lat-metadata",
881                         title="Copy",
882                         style={"display": "inline-block"}
883                     ),
884                     title
885                 ]),
886                 dbc.CardBody(
887                     dbc.ListGroup(list_group_items, flush=True),
888                     id="tput-lat-metadata",
889                     class_name="p-0"
890                 )
891             ]
892         )
893     ]
894
895     return metadata, graph, True