fix(etl): Typo"
[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     children = list()
484     if C.START_TRENDING:
485         children.append(dbc.NavItem(dbc.NavLink(
486             C.TREND_TITLE,
487             active=active[0],
488             external_link=True,
489             href="/trending"
490         )))
491     if C.START_FAILURES:
492         children.append(dbc.NavItem(dbc.NavLink(
493             C.NEWS_TITLE,
494             active=active[1],
495             external_link=True,
496             href="/news"
497         )))
498     if C.START_STATISTICS:
499         children.append(dbc.NavItem(dbc.NavLink(
500             C.STATS_TITLE,
501             active=active[2],
502             external_link=True,
503             href="/stats"
504         )))
505     if C.START_SEARCH:
506         children.append(dbc.NavItem(dbc.NavLink(
507             C.SEARCH_TITLE,
508             active=active[3],
509             external_link=True,
510             href="/search"
511         )))
512     if C.START_DOC:
513         children.append(dbc.NavItem(dbc.NavLink(
514             "Documentation",
515             id="btn-documentation",
516         )))
517     return dbc.NavbarSimple(
518         children=children,
519         id="navbarsimple-main",
520         brand=C.BRAND,
521         brand_href="/",
522         brand_external_link=True,
523         class_name="p-2",
524         fluid=True
525     )
526
527
528 def navbar_report(active: tuple):
529     """Add nav element with navigation panel. It is placed on the top.
530
531     :param active: Tuple of boolean values defining the active items in the
532         navbar. True == active
533     :type active: tuple
534     :returns: Navigation bar.
535     :rtype: dbc.NavbarSimple
536     """
537     children = list()
538     if C.START_REPORT:
539         children.append(dbc.NavItem(dbc.NavLink(
540             C.REPORT_TITLE,
541             active=active[0],
542             external_link=True,
543             href="/report"
544         )))
545     if C.START_COMPARISONS:
546         children.append(dbc.NavItem(dbc.NavLink(
547             "Comparisons",
548             active=active[1],
549             external_link=True,
550             href="/comparisons"
551         )))
552     if C.START_COVERAGE:
553         children.append(dbc.NavItem(dbc.NavLink(
554             "Coverage Data",
555             active=active[2],
556             external_link=True,
557             href="/coverage"
558         )))
559     if C.START_SEARCH:
560         children.append(dbc.NavItem(dbc.NavLink(
561             C.SEARCH_TITLE,
562             active=active[3],
563             external_link=True,
564             href="/search"
565         )))
566     if C.START_DOC:
567         children.append(dbc.NavItem(dbc.NavLink(
568             "Documentation",
569             id="btn-documentation",
570         )))
571     return dbc.NavbarSimple(
572         children=children,
573         id="navbarsimple-main",
574         brand=C.BRAND,
575         brand_href="/",
576         brand_external_link=True,
577         class_name="p-2",
578         fluid=True
579     )
580
581
582 def filter_table_data(
583         store_table_data: list,
584         table_filter: str
585     ) -> list:
586     """Filter table data using user specified filter.
587
588     :param store_table_data: Table data represented as a list of records.
589     :param table_filter: User specified filter.
590     :type store_table_data: list
591     :type table_filter: str
592     :returns: A new table created by filtering of table data represented as
593         a list of records.
594     :rtype: list
595     """
596
597     # Checks:
598     if not any((table_filter, store_table_data, )):
599         return store_table_data
600
601     def _split_filter_part(filter_part: str) -> tuple:
602         """Split a part of filter into column name, operator and value.
603         A "part of filter" is a sting berween "&&" operator.
604
605         :param filter_part: A part of filter.
606         :type filter_part: str
607         :returns: Column name, operator, value
608         :rtype: tuple[str, str, str|float]
609         """
610         for operator_type in C.OPERATORS:
611             for operator in operator_type:
612                 if operator in filter_part:
613                     name_p, val_p = filter_part.split(operator, 1)
614                     name = name_p[name_p.find("{") + 1 : name_p.rfind("}")]
615                     val_p = val_p.strip()
616                     if (val_p[0] == val_p[-1] and val_p[0] in ("'", '"', '`')):
617                         value = val_p[1:-1].replace("\\" + val_p[0], val_p[0])
618                     else:
619                         try:
620                             value = float(val_p)
621                         except ValueError:
622                             value = val_p
623
624                     return name, operator_type[0].strip(), value
625         return (None, None, None)
626
627     df = pd.DataFrame.from_records(store_table_data)
628     for filter_part in table_filter.split(" && "):
629         col_name, operator, filter_value = _split_filter_part(filter_part)
630         if operator == "contains":
631             df = df.loc[df[col_name].str.contains(filter_value, regex=True)]
632         elif operator in ("eq", "ne", "lt", "le", "gt", "ge"):
633             # These operators match pandas series operator method names.
634             df = df.loc[getattr(df[col_name], operator)(filter_value)]
635         elif operator == "datestartswith":
636             # This is a simplification of the front-end filtering logic,
637             # only works with complete fields in standard format.
638             # Currently not used in comparison tables.
639             df = df.loc[df[col_name].str.startswith(filter_value)]
640
641     return df.to_dict("records")
642
643
644 def sort_table_data(
645         store_table_data: list,
646         sort_by: list
647     ) -> list:
648     """Sort table data using user specified order.
649
650     :param store_table_data: Table data represented as a list of records.
651     :param sort_by: User specified sorting order (multicolumn).
652     :type store_table_data: list
653     :type sort_by: list
654     :returns: A new table created by sorting the table data represented as
655         a list of records.
656     :rtype: list
657     """
658
659     # Checks:
660     if not any((sort_by, store_table_data, )):
661         return store_table_data
662
663     df = pd.DataFrame.from_records(store_table_data)
664     if len(sort_by):
665         dff = df.sort_values(
666             [col["column_id"] for col in sort_by],
667             ascending=[col["direction"] == "asc" for col in sort_by],
668             inplace=False
669         )
670     else:
671         # No sort is applied
672         dff = df
673
674     return dff.to_dict("records")
675
676
677 def show_trending_graph_data(
678         trigger: Trigger,
679         data: dict,
680         graph_layout: dict
681     ) -> tuple:
682     """Generates the data for the offcanvas displayed when a particular point in
683     a trending graph (daily data) is clicked on.
684
685     :param trigger: The information from trigger when the data point is clicked
686         on.
687     :param graph: The data from the clicked point in the graph.
688     :param graph_layout: The layout of the HDRH latency graph.
689     :type trigger: Trigger
690     :type graph: dict
691     :type graph_layout: dict
692     :returns: The data to be displayed on the offcanvas and the information to
693         show the offcanvas.
694     :rtype: tuple(list, list, bool)
695     """
696
697     if trigger.idx == "tput":
698         idx = 0
699     elif trigger.idx == "bandwidth":
700         idx = 1
701     elif trigger.idx == "lat":
702         idx = len(data) - 1
703     else:
704         return list(), list(), False
705     try:
706         data = data[idx]["points"][0]
707     except (IndexError, KeyError, ValueError, TypeError):
708         return list(), list(), False
709
710     metadata = no_update
711     graph = list()
712
713     list_group_items = list()
714     for itm in data.get("text", None).split("<br>"):
715         if not itm:
716             continue
717         lst_itm = itm.split(": ")
718         if lst_itm[0] == "csit-ref":
719             list_group_item = dbc.ListGroupItem([
720                 dbc.Badge(lst_itm[0]),
721                 html.A(
722                     lst_itm[1],
723                     href=f"{C.URL_LOGS}{lst_itm[1]}",
724                     target="_blank"
725                 )
726             ])
727         else:
728             list_group_item = dbc.ListGroupItem([
729                 dbc.Badge(lst_itm[0]),
730                 lst_itm[1]
731             ])
732         list_group_items.append(list_group_item)
733
734     if trigger.idx == "tput":
735         title = "Throughput"
736     elif trigger.idx == "bandwidth":
737         title = "Bandwidth"
738     elif trigger.idx == "lat":
739         title = "Latency"
740         hdrh_data = data.get("customdata", None)
741         if hdrh_data:
742             graph = [dbc.Card(
743                 class_name="gy-2 p-0",
744                 children=[
745                     dbc.CardHeader(hdrh_data.pop("name")),
746                     dbc.CardBody(
747                         dcc.Graph(
748                             id="hdrh-latency-graph",
749                             figure=graph_hdrh_latency(hdrh_data, graph_layout)
750                         )
751                     )
752                 ])
753             ]
754
755     metadata = [
756         dbc.Card(
757             class_name="gy-2 p-0",
758             children=[
759                 dbc.CardHeader(children=[
760                     dcc.Clipboard(
761                         target_id="tput-lat-metadata",
762                         title="Copy",
763                         style={"display": "inline-block"}
764                     ),
765                     title
766                 ]),
767                 dbc.CardBody(
768                     dbc.ListGroup(list_group_items, flush=True),
769                     id="tput-lat-metadata",
770                     class_name="p-0",
771                 )
772             ]
773         )
774     ]
775
776     return metadata, graph, True
777
778
779 def show_iterative_graph_data(
780         trigger: Trigger,
781         data: dict,
782         graph_layout: dict
783     ) -> tuple:
784     """Generates the data for the offcanvas displayed when a particular point in
785     a box graph (iterative data) is clicked on.
786
787     :param trigger: The information from trigger when the data point is clicked
788         on.
789     :param graph: The data from the clicked point in the graph.
790     :param graph_layout: The layout of the HDRH latency graph.
791     :type trigger: Trigger
792     :type graph: dict
793     :type graph_layout: dict
794     :returns: The data to be displayed on the offcanvas and the information to
795         show the offcanvas.
796     :rtype: tuple(list, list, bool)
797     """
798
799     if trigger.idx == "tput":
800         idx = 0
801     elif trigger.idx == "bandwidth":
802         idx = 1
803     elif trigger.idx == "lat":
804         idx = len(data) - 1
805     else:
806         return list(), list(), False
807
808     try:
809         data = data[idx]["points"]
810     except (IndexError, KeyError, ValueError, TypeError):
811         return list(), list(), False
812
813     def _process_stats(data: list, param: str) -> list:
814         """Process statistical data provided by plot.ly box graph.
815
816         :param data: Statistical data provided by plot.ly box graph.
817         :param param: Parameter saying if the data come from "tput" or
818             "lat" graph.
819         :type data: list
820         :type param: str
821         :returns: Listo of tuples where the first value is the
822             statistic's name and the secont one it's value.
823         :rtype: list
824         """
825         if len(data) == 7:
826             stats = ("max", "upper fence", "q3", "median", "q1",
827                     "lower fence", "min")
828         elif len(data) == 9:
829             stats = ("outlier", "max", "upper fence", "q3", "median",
830                     "q1", "lower fence", "min", "outlier")
831         elif len(data) == 1:
832             if param == "lat":
833                 stats = ("average latency at 50% PDR", )
834             elif param == "bandwidth":
835                 stats = ("bandwidth", )
836             else:
837                 stats = ("throughput", )
838         else:
839             return list()
840         unit = " [us]" if param == "lat" else str()
841         return [(f"{stat}{unit}", f"{value['y']:,.0f}")
842                 for stat, value in zip(stats, data)]
843
844     customdata = data[0].get("customdata", dict())
845     datapoint = customdata.get("metadata", dict())
846     hdrh_data = customdata.get("hdrh", dict())
847
848     list_group_items = list()
849     for k, v in datapoint.items():
850         if k == "csit-ref":
851             if len(data) > 1:
852                 continue
853             list_group_item = dbc.ListGroupItem([
854                 dbc.Badge(k),
855                 html.A(v, href=f"{C.URL_LOGS}{v}", target="_blank")
856             ])
857         else:
858             list_group_item = dbc.ListGroupItem([dbc.Badge(k), v])
859         list_group_items.append(list_group_item)
860
861     graph = list()
862     if trigger.idx == "tput":
863         title = "Throughput"
864     elif trigger.idx == "bandwidth":
865         title = "Bandwidth"
866     elif trigger.idx == "lat":
867         title = "Latency"
868         if len(data) == 1:
869             if hdrh_data:
870                 graph = [dbc.Card(
871                     class_name="gy-2 p-0",
872                     children=[
873                         dbc.CardHeader(hdrh_data.pop("name")),
874                         dbc.CardBody(dcc.Graph(
875                             id="hdrh-latency-graph",
876                             figure=graph_hdrh_latency(hdrh_data, graph_layout)
877                         ))
878                     ])
879                 ]
880
881     for k, v in _process_stats(data, trigger.idx):
882         list_group_items.append(dbc.ListGroupItem([dbc.Badge(k), v]))
883
884     metadata = [
885         dbc.Card(
886             class_name="gy-2 p-0",
887             children=[
888                 dbc.CardHeader(children=[
889                     dcc.Clipboard(
890                         target_id="tput-lat-metadata",
891                         title="Copy",
892                         style={"display": "inline-block"}
893                     ),
894                     title
895                 ]),
896                 dbc.CardBody(
897                     dbc.ListGroup(list_group_items, flush=True),
898                     id="tput-lat-metadata",
899                     class_name="p-0"
900                 )
901             ]
902         )
903     ]
904
905     return metadata, graph, True