UTI: Set params in ctrl panel from url
[csit.git] / resources / tools / dash / app / pal / trending / layout.py
1 # Copyright (c) 2022 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 """Plotly Dash HTML layout override.
15 """
16
17 import logging
18 import pandas as pd
19 import dash_bootstrap_components as dbc
20
21 from flask import Flask
22 from dash import dcc
23 from dash import html
24 from dash import callback_context, no_update, ALL
25 from dash import Input, Output, State
26 from dash.exceptions import PreventUpdate
27 from yaml import load, FullLoader, YAMLError
28 from datetime import datetime, timedelta
29 from copy import deepcopy
30 from json import loads, JSONDecodeError
31 from ast import literal_eval
32
33 from ..utils.constants import Constants as C
34 from ..utils.utils import show_tooltip, label, sync_checklists, list_tests, \
35     get_date, gen_new_url, generate_options
36 from ..utils.url_processing import url_decode
37 from ..data.data import Data
38 from .graphs import graph_trending, graph_hdrh_latency, \
39     select_trending_data
40
41
42 class Layout:
43     """The layout of the dash app and the callbacks.
44     """
45
46     def __init__(self, app: Flask, html_layout_file: str,
47         graph_layout_file: str, data_spec_file: str, tooltip_file: str,
48         time_period: str=None) -> None:
49         """Initialization:
50         - save the input parameters,
51         - read and pre-process the data,
52         - prepare data for the control panel,
53         - read HTML layout file,
54         - read tooltips from the tooltip file.
55
56         :param app: Flask application running the dash application.
57         :param html_layout_file: Path and name of the file specifying the HTML
58             layout of the dash application.
59         :param graph_layout_file: Path and name of the file with layout of
60             plot.ly graphs.
61         :param data_spec_file: Path and name of the file specifying the data to
62             be read from parquets for this application.
63         :param tooltip_file: Path and name of the yaml file specifying the
64             tooltips.
65         :param time_period: It defines the time period for data read from the
66             parquets in days from now back to the past.
67         :type app: Flask
68         :type html_layout_file: str
69         :type graph_layout_file: str
70         :type data_spec_file: str
71         :type tooltip_file: str
72         :type time_period: int
73         """
74
75         # Inputs
76         self._app = app
77         self._html_layout_file = html_layout_file
78         self._graph_layout_file = graph_layout_file
79         self._data_spec_file = data_spec_file
80         self._tooltip_file = tooltip_file
81         self._time_period = time_period
82
83         # Read the data:
84         data_mrr = Data(
85             data_spec_file=self._data_spec_file,
86             debug=True
87         ).read_trending_mrr(days=self._time_period)
88
89         data_ndrpdr = Data(
90             data_spec_file=self._data_spec_file,
91             debug=True
92         ).read_trending_ndrpdr(days=self._time_period)
93
94         self._data = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
95
96         data_time_period = \
97             (datetime.utcnow() - self._data["start_time"].min()).days
98         if self._time_period > data_time_period:
99             self._time_period = data_time_period
100
101
102         # Get structure of tests:
103         tbs = dict()
104         for _, row in self._data[["job", "test_id"]].drop_duplicates().\
105                 iterrows():
106             lst_job = row["job"].split("-")
107             dut = lst_job[1]
108             ttype = lst_job[3]
109             tbed = "-".join(lst_job[-2:])
110             lst_test = row["test_id"].split(".")
111             if dut == "dpdk":
112                 area = "dpdk"
113             else:
114                 area = "-".join(lst_test[3:-2])
115             suite = lst_test[-2].replace("2n1l-", "").replace("1n1l-", "").\
116                 replace("2n-", "")
117             test = lst_test[-1]
118             nic = suite.split("-")[0]
119             for drv in C.DRIVERS:
120                 if drv in test:
121                     if drv == "af-xdp":
122                         driver = "af_xdp"
123                     else:
124                         driver = drv
125                     test = test.replace(f"{drv}-", "")
126                     break
127             else:
128                 driver = "dpdk"
129             infra = "-".join((tbed, nic, driver))
130             lst_test = test.split("-")
131             framesize = lst_test[0]
132             core = lst_test[1] if lst_test[1] else "8C"
133             test = "-".join(lst_test[2: -1])
134
135             if tbs.get(dut, None) is None:
136                 tbs[dut] = dict()
137             if tbs[dut].get(infra, None) is None:
138                 tbs[dut][infra] = dict()
139             if tbs[dut][infra].get(area, None) is None:
140                 tbs[dut][infra][area] = dict()
141             if tbs[dut][infra][area].get(test, None) is None:
142                 tbs[dut][infra][area][test] = dict()
143                 tbs[dut][infra][area][test]["core"] = list()
144                 tbs[dut][infra][area][test]["frame-size"] = list()
145                 tbs[dut][infra][area][test]["test-type"] = list()
146             if core.upper() not in tbs[dut][infra][area][test]["core"]:
147                 tbs[dut][infra][area][test]["core"].append(core.upper())
148             if framesize.upper() not in \
149                     tbs[dut][infra][area][test]["frame-size"]:
150                 tbs[dut][infra][area][test]["frame-size"].append(
151                     framesize.upper())
152             if ttype == "mrr":
153                 if "MRR" not in tbs[dut][infra][area][test]["test-type"]:
154                     tbs[dut][infra][area][test]["test-type"].append("MRR")
155             elif ttype == "ndrpdr":
156                 if "NDR" not in tbs[dut][infra][area][test]["test-type"]:
157                     tbs[dut][infra][area][test]["test-type"].extend(
158                         ("NDR", "PDR"))
159         self._spec_tbs = tbs
160
161         # Read from files:
162         self._html_layout = ""
163         self._graph_layout = None
164         self._tooltips = dict()
165
166         try:
167             with open(self._html_layout_file, "r") as file_read:
168                 self._html_layout = file_read.read()
169         except IOError as err:
170             raise RuntimeError(
171                 f"Not possible to open the file {self._html_layout_file}\n{err}"
172             )
173
174         try:
175             with open(self._graph_layout_file, "r") as file_read:
176                 self._graph_layout = load(file_read, Loader=FullLoader)
177         except IOError as err:
178             raise RuntimeError(
179                 f"Not possible to open the file {self._graph_layout_file}\n"
180                 f"{err}"
181             )
182         except YAMLError as err:
183             raise RuntimeError(
184                 f"An error occurred while parsing the specification file "
185                 f"{self._graph_layout_file}\n{err}"
186             )
187
188         try:
189             with open(self._tooltip_file, "r") as file_read:
190                 self._tooltips = load(file_read, Loader=FullLoader)
191         except IOError as err:
192             logging.warning(
193                 f"Not possible to open the file {self._tooltip_file}\n{err}"
194             )
195         except YAMLError as err:
196             logging.warning(
197                 f"An error occurred while parsing the specification file "
198                 f"{self._tooltip_file}\n{err}"
199             )
200
201         # Callbacks:
202         if self._app is not None and hasattr(self, 'callbacks'):
203             self.callbacks(self._app)
204
205     @property
206     def html_layout(self):
207         return self._html_layout
208
209     @property
210     def spec_tbs(self):
211         return self._spec_tbs
212
213     @property
214     def data(self):
215         return self._data
216
217     @property
218     def layout(self):
219         return self._graph_layout
220
221     @property
222     def time_period(self):
223         return self._time_period
224
225     def add_content(self):
226         """Top level method which generated the web page.
227
228         It generates:
229         - Store for user input data,
230         - Navigation bar,
231         - Main area with control panel and ploting area.
232
233         If no HTML layout is provided, an error message is displayed instead.
234
235         :returns: The HTML div with the whole page.
236         :rtype: html.Div
237         """
238
239         if self.html_layout and self.spec_tbs:
240             return html.Div(
241                 id="div-main",
242                 children=[
243                     dbc.Row(
244                         id="row-navbar",
245                         class_name="g-0",
246                         children=[
247                             self._add_navbar(),
248                         ]
249                     ),
250                     dcc.Loading(
251                         dbc.Offcanvas(
252                             class_name="w-50",
253                             id="offcanvas-metadata",
254                             title="Throughput And Latency",
255                             placement="end",
256                             is_open=False,
257                             children=[
258                                 dbc.Row(id="metadata-tput-lat"),
259                                 dbc.Row(id="metadata-hdrh-graph"),
260                             ]
261                         )
262                     ),
263                     dbc.Row(
264                         id="row-main",
265                         class_name="g-0",
266                         children=[
267                             dcc.Store(id="selected-tests"),
268                             dcc.Store(id="control-panel"),
269                             dcc.Location(id="url", refresh=False),
270                             self._add_ctrl_col(),
271                             self._add_plotting_col(),
272                         ]
273                     )
274                 ]
275             )
276         else:
277             return html.Div(
278                 id="div-main-error",
279                 children=[
280                     dbc.Alert(
281                         [
282                             "An Error Occured",
283                         ],
284                         color="danger",
285                     ),
286                 ]
287             )
288
289     def _add_navbar(self):
290         """Add nav element with navigation panel. It is placed on the top.
291
292         :returns: Navigation bar.
293         :rtype: dbc.NavbarSimple
294         """
295         return dbc.NavbarSimple(
296             id="navbarsimple-main",
297             children=[
298                 dbc.NavItem(
299                     dbc.NavLink(
300                         "Continuous Performance Trending",
301                         disabled=True,
302                         external_link=True,
303                         href="#"
304                     )
305                 )
306             ],
307             brand="Dashboard",
308             brand_href="/",
309             brand_external_link=True,
310             class_name="p-2",
311             fluid=True,
312         )
313
314     def _add_ctrl_col(self) -> dbc.Col:
315         """Add column with controls. It is placed on the left side.
316
317         :returns: Column with the control panel.
318         :rtype: dbc.Col
319         """
320         return dbc.Col(
321             id="col-controls",
322             children=[
323                 self._add_ctrl_panel(),
324             ],
325         )
326
327     def _add_plotting_col(self) -> dbc.Col:
328         """Add column with plots and tables. It is placed on the right side.
329
330         :returns: Column with tables.
331         :rtype: dbc.Col
332         """
333         return dbc.Col(
334             id="col-plotting-area",
335             children=[
336                 dcc.Loading(
337                     children=[
338                         dbc.Row(  # Throughput
339                             id="row-graph-tput",
340                             class_name="g-0 p-2",
341                             children=[
342                                 C.PLACEHOLDER
343                             ]
344                         ),
345                         dbc.Row(  # Latency
346                             id="row-graph-lat",
347                             class_name="g-0 p-2",
348                             children=[
349                                 C.PLACEHOLDER
350                             ]
351                         ),
352                         dbc.Row(  # Download
353                             id="row-btn-download",
354                             class_name="g-0 p-2",
355                             children=[
356                                 C.PLACEHOLDER
357                             ]
358                         )
359                     ]
360                 )
361             ],
362             width=9,
363         )
364
365     def _add_ctrl_panel(self) -> dbc.Row:
366         """Add control panel.
367
368         :returns: Control panel.
369         :rtype: dbc.Row
370         """
371         return dbc.Row(
372             id="row-ctrl-panel",
373             class_name="g-0 p-2",
374             children=[
375                 dbc.Row(
376                     class_name="g-0",
377                     children=[
378                         dbc.InputGroup(
379                             [
380                                 dbc.InputGroupText(
381                                     children=show_tooltip(self._tooltips,
382                                         "help-dut", "DUT")
383                                 ),
384                                 dbc.Select(
385                                     id="dd-ctrl-dut",
386                                     placeholder=(
387                                         "Select a Device under Test..."
388                                     ),
389                                     options=sorted(
390                                         [
391                                             {"label": k, "value": k} \
392                                                 for k in self.spec_tbs.keys()
393                                         ],
394                                         key=lambda d: d["label"]
395                                     )
396                                 )
397                             ],
398                             class_name="mb-3",
399                             size="sm",
400                         ),
401                     ]
402                 ),
403                 dbc.Row(
404                     class_name="g-0",
405                     children=[
406                         dbc.InputGroup(
407                             [
408                                 dbc.InputGroupText(
409                                     children=show_tooltip(self._tooltips,
410                                         "help-infra", "Infra")
411                                 ),
412                                 dbc.Select(
413                                     id="dd-ctrl-phy",
414                                     placeholder=(
415                                         "Select a Physical Test Bed "
416                                         "Topology..."
417                                     )
418                                 )
419                             ],
420                             class_name="mb-3",
421                             size="sm",
422                         ),
423                     ]
424                 ),
425                 dbc.Row(
426                     class_name="g-0",
427                     children=[
428                         dbc.InputGroup(
429                             [
430                                 dbc.InputGroupText(
431                                     children=show_tooltip(self._tooltips,
432                                         "help-area", "Area")
433                                 ),
434                                 dbc.Select(
435                                     id="dd-ctrl-area",
436                                     placeholder="Select an Area...",
437                                     disabled=True,
438                                 ),
439                             ],
440                             class_name="mb-3",
441                             size="sm",
442                         ),
443                     ]
444                 ),
445                 dbc.Row(
446                     class_name="g-0",
447                     children=[
448                         dbc.InputGroup(
449                             [
450                                 dbc.InputGroupText(
451                                     children=show_tooltip(self._tooltips,
452                                         "help-test", "Test")
453                                 ),
454                                 dbc.Select(
455                                     id="dd-ctrl-test",
456                                     placeholder="Select a Test...",
457                                     disabled=True,
458                                 ),
459                             ],
460                             class_name="mb-3",
461                             size="sm",
462                         ),
463                     ]
464                 ),
465                 dbc.Row(
466                     id="row-ctrl-framesize",
467                     class_name="gy-1",
468                     children=[
469                         dbc.Label(
470                             children=show_tooltip(self._tooltips,
471                                 "help-framesize", "Frame Size"),
472                             class_name="p-0"
473                         ),
474                         dbc.Col(
475                             children=[
476                                 dbc.Checklist(
477                                     id="cl-ctrl-framesize-all",
478                                     options=C.CL_ALL_DISABLED,
479                                     inline=True,
480                                     switch=False
481                                 ),
482                             ],
483                             width=3
484                         ),
485                         dbc.Col(
486                             children=[
487                                 dbc.Checklist(
488                                     id="cl-ctrl-framesize",
489                                     inline=True,
490                                     switch=False
491                                 )
492                             ]
493                         )
494                     ]
495                 ),
496                 dbc.Row(
497                     id="row-ctrl-core",
498                     class_name="gy-1",
499                     children=[
500                         dbc.Label(
501                             children=show_tooltip(self._tooltips,
502                                 "help-cores", "Number of Cores"),
503                             class_name="p-0"
504                         ),
505                         dbc.Col(
506                             children=[
507                                 dbc.Checklist(
508                                     id="cl-ctrl-core-all",
509                                     options=C.CL_ALL_DISABLED,
510                                     inline=False,
511                                     switch=False
512                                 )
513                             ],
514                             width=3
515                         ),
516                         dbc.Col(
517                             children=[
518                                 dbc.Checklist(
519                                     id="cl-ctrl-core",
520                                     inline=True,
521                                     switch=False
522                                 )
523                             ]
524                         )
525                     ]
526                 ),
527                 dbc.Row(
528                     id="row-ctrl-testtype",
529                     class_name="gy-1",
530                     children=[
531                         dbc.Label(
532                             children=show_tooltip(self._tooltips,
533                                 "help-ttype", "Test Type"),
534                             class_name="p-0"
535                         ),
536                         dbc.Col(
537                             children=[
538                                 dbc.Checklist(
539                                     id="cl-ctrl-testtype-all",
540                                     options=C.CL_ALL_DISABLED,
541                                     inline=True,
542                                     switch=False
543                                 ),
544                             ],
545                             width=3
546                         ),
547                         dbc.Col(
548                             children=[
549                                 dbc.Checklist(
550                                     id="cl-ctrl-testtype",
551                                     inline=True,
552                                     switch=False
553                                 )
554                             ]
555                         )
556                     ]
557                 ),
558                 dbc.Row(
559                     id="row-ctrl-normalize",
560                     class_name="gy-1",
561                     children=[
562                         dbc.Label(
563                             children=show_tooltip(self._tooltips,
564                                 "help-normalize", "Normalize"),
565                             class_name="p-0"
566                         ),
567                         dbc.Col(
568                             children=[
569                                 dbc.Checklist(
570                                     id="cl-ctrl-normalize",
571                                     options=[{
572                                         "value": "normalize",
573                                         "label": (
574                                             "Normalize results to CPU"
575                                             "frequency 2GHz"
576                                         )
577                                     }],
578                                     value=[],
579                                     inline=True,
580                                     switch=False
581                                 ),
582                             ]
583                         )
584                     ]
585                 ),
586                 dbc.Row(
587                     class_name="gy-1 p-0",
588                     children=[
589                         dbc.ButtonGroup(
590                             [
591                                 dbc.Button(
592                                     id="btn-ctrl-add",
593                                     children="Add Selected",
594                                     class_name="me-1",
595                                     color="info"
596                                 )
597                             ],
598                             size="md",
599                         )
600                     ]
601                 ),
602                 dbc.Row(
603                     class_name="gy-1",
604                     children=[
605                         dbc.Label(
606                             class_name="gy-1",
607                             children=show_tooltip(self._tooltips,
608                                 "help-time-period", "Time Period"),
609                         ),
610                         dcc.DatePickerRange(
611                             id="dpr-period",
612                             className="d-flex justify-content-center",
613                             min_date_allowed=\
614                                 datetime.utcnow() - timedelta(
615                                     days=self.time_period),
616                             max_date_allowed=datetime.utcnow(),
617                             initial_visible_month=datetime.utcnow(),
618                             start_date=\
619                                 datetime.utcnow() - timedelta(
620                                     days=self.time_period),
621                             end_date=datetime.utcnow(),
622                             display_format="D MMM YY"
623                         )
624                     ]
625                 ),
626                 dbc.Row(
627                     id="row-card-sel-tests",
628                     class_name="gy-1",
629                     style=C.STYLE_DISABLED,
630                     children=[
631                         dbc.Label(
632                             "Selected tests",
633                             class_name="p-0"
634                         ),
635                         dbc.Checklist(
636                             class_name="overflow-auto",
637                             id="cl-selected",
638                             options=[],
639                             inline=False,
640                             style={"max-height": "12em"},
641                         )
642                     ],
643                 ),
644                 dbc.Row(
645                     id="row-btns-sel-tests",
646                     style=C.STYLE_DISABLED,
647                     children=[
648                         dbc.ButtonGroup(
649                             class_name="gy-2",
650                             children=[
651                                 dbc.Button(
652                                     id="btn-sel-remove",
653                                     children="Remove Selected",
654                                     class_name="w-100 me-1",
655                                     color="info",
656                                     disabled=False
657                                 ),
658                                 dbc.Button(
659                                     id="btn-sel-remove-all",
660                                     children="Remove All",
661                                     class_name="w-100 me-1",
662                                     color="info",
663                                     disabled=False
664                                 ),
665                             ],
666                             size="md",
667                         )
668                     ]
669                 ),
670             ]
671         )
672
673     class ControlPanel:
674         """A class representing the control panel.
675         """
676
677         def __init__(self, panel: dict) -> None:
678             """Initialisation of the control pannel by default values. If
679             particular values are provided (parameter "panel") they are set
680             afterwards.
681
682             :param panel: Custom values to be set to the control panel.
683             :param default: Default values to be set to the control panel.
684             :type panel: dict
685             :type defaults: dict
686             """
687
688             # Defines also the order of keys
689             self._defaults = {
690                 "dd-ctrl-dut-value": str(),
691                 "dd-ctrl-phy-options": list(),
692                 "dd-ctrl-phy-disabled": True,
693                 "dd-ctrl-phy-value": str(),
694                 "dd-ctrl-area-options": list(),
695                 "dd-ctrl-area-disabled": True,
696                 "dd-ctrl-area-value": str(),
697                 "dd-ctrl-test-options": list(),
698                 "dd-ctrl-test-disabled": True,
699                 "dd-ctrl-test-value": str(),
700                 "cl-ctrl-core-options": list(),
701                 "cl-ctrl-core-value": list(),
702                 "cl-ctrl-core-all-value": list(),
703                 "cl-ctrl-core-all-options": C.CL_ALL_DISABLED,
704                 "cl-ctrl-framesize-options": list(),
705                 "cl-ctrl-framesize-value": list(),
706                 "cl-ctrl-framesize-all-value": list(),
707                 "cl-ctrl-framesize-all-options": C.CL_ALL_DISABLED,
708                 "cl-ctrl-testtype-options": list(),
709                 "cl-ctrl-testtype-value": list(),
710                 "cl-ctrl-testtype-all-value": list(),
711                 "cl-ctrl-testtype-all-options": C.CL_ALL_DISABLED,
712                 "btn-ctrl-add-disabled": True,
713                 "cl-normalize-value": list(),
714                 "cl-selected-options": list(),
715             }
716
717             self._panel = deepcopy(self._defaults)
718             if panel:
719                 for key in self._defaults:
720                     self._panel[key] = panel[key]
721
722         @property
723         def defaults(self) -> dict:
724             return self._defaults
725
726         @property
727         def panel(self) -> dict:
728             return self._panel
729
730         def set(self, kwargs: dict) -> None:
731             """Set the values of the Control panel.
732
733             :param kwargs: key - value pairs to be set.
734             :type kwargs: dict
735             :raises KeyError: If the key in kwargs is not present in the Control
736                 panel.
737             """
738             for key, val in kwargs.items():
739                 if key in self._panel:
740                     self._panel[key] = val
741                 else:
742                     raise KeyError(f"The key {key} is not defined.")
743
744         def get(self, key: str) -> any:
745             """Returns the value of a key from the Control panel.
746
747             :param key: The key which value should be returned.
748             :type key: str
749             :returns: The value of the key.
750             :rtype: any
751             :raises KeyError: If the key in kwargs is not present in the Control
752                 panel.
753             """
754             return self._panel[key]
755
756         def values(self) -> tuple:
757             """Returns the values from the Control panel as a list.
758
759             :returns: The values from the Control panel.
760             :rtype: list
761             """
762             return tuple(self._panel.values())
763
764     def callbacks(self, app):
765         """Callbacks for the whole application.
766
767         :param app: The application.
768         :type app: Flask
769         """
770
771         def _generate_plotting_area(figs: tuple, url: str) -> tuple:
772             """Generate the plotting area with all its content.
773
774             :param figs: Figures to be placed in the plotting area.
775             :param utl: The URL to be placed in the plotting area bellow the
776                 tables.
777             :type figs: tuple of plotly.graph_objects.Figure
778             :type url: str
779             :returns: tuple of elements to be shown in the plotting area.
780             :rtype: tuple(dcc.Graph, dcc.Graph, list(dbc.Col, dbc.Col))
781             """
782
783             (fig_tput, fig_lat) = figs
784
785             row_fig_tput = C.PLACEHOLDER
786             row_fig_lat = C.PLACEHOLDER
787             row_btn_dwnld = C.PLACEHOLDER
788
789             if fig_tput:
790                 row_fig_tput = [
791                     dcc.Graph(
792                         id={"type": "graph", "index": "tput"},
793                         figure=fig_tput
794                     )
795                 ]
796                 row_btn_dwnld = [
797                     dbc.Col(  # Download
798                         width=2,
799                         children=[
800                             dcc.Loading(children=[
801                                 dbc.Button(
802                                     id="btn-download-data",
803                                     children=show_tooltip(self._tooltips,
804                                         "help-download", "Download Data"),
805                                     class_name="me-1",
806                                     color="info"
807                                 ),
808                                 dcc.Download(id="download-data")
809                             ]),
810                         ]
811                     ),
812                     dbc.Col(  # Show URL
813                         width=10,
814                         children=[
815                             dbc.InputGroup(
816                                 class_name="me-1",
817                                 children=[
818                                     dbc.InputGroupText(
819                                         style=C.URL_STYLE,
820                                         children=show_tooltip(self._tooltips,
821                                             "help-url", "URL", "input-url")
822                                     ),
823                                     dbc.Input(
824                                         id="input-url",
825                                         readonly=True,
826                                         type="url",
827                                         style=C.URL_STYLE,
828                                         value=url
829                                     )
830                                 ]
831                             )
832                         ]
833                     )
834                 ]
835             if fig_lat:
836                 row_fig_lat = [
837                     dcc.Graph(
838                         id={"type": "graph", "index": "lat"},
839                         figure=fig_lat
840                     )
841                 ]
842
843             return row_fig_tput, row_fig_lat, row_btn_dwnld
844
845         @app.callback(
846             Output("control-panel", "data"),  # Store
847             Output("selected-tests", "data"),  # Store
848             Output("row-graph-tput", "children"),
849             Output("row-graph-lat", "children"),
850             Output("row-btn-download", "children"),
851             Output("row-card-sel-tests", "style"),
852             Output("row-btns-sel-tests", "style"),
853             Output("dd-ctrl-dut", "value"),
854             Output("dd-ctrl-phy", "options"),
855             Output("dd-ctrl-phy", "disabled"),
856             Output("dd-ctrl-phy", "value"),
857             Output("dd-ctrl-area", "options"),
858             Output("dd-ctrl-area", "disabled"),
859             Output("dd-ctrl-area", "value"),
860             Output("dd-ctrl-test", "options"),
861             Output("dd-ctrl-test", "disabled"),
862             Output("dd-ctrl-test", "value"),
863             Output("cl-ctrl-core", "options"),
864             Output("cl-ctrl-core", "value"),
865             Output("cl-ctrl-core-all", "value"),
866             Output("cl-ctrl-core-all", "options"),
867             Output("cl-ctrl-framesize", "options"),
868             Output("cl-ctrl-framesize", "value"),
869             Output("cl-ctrl-framesize-all", "value"),
870             Output("cl-ctrl-framesize-all", "options"),
871             Output("cl-ctrl-testtype", "options"),
872             Output("cl-ctrl-testtype", "value"),
873             Output("cl-ctrl-testtype-all", "value"),
874             Output("cl-ctrl-testtype-all", "options"),
875             Output("btn-ctrl-add", "disabled"),
876             Output("cl-ctrl-normalize", "value"),
877             Output("cl-selected", "options"),  # User selection
878             State("control-panel", "data"),  # Store
879             State("selected-tests", "data"),  # Store
880             State("cl-selected", "value"),  # User selection
881             Input("dd-ctrl-dut", "value"),
882             Input("dd-ctrl-phy", "value"),
883             Input("dd-ctrl-area", "value"),
884             Input("dd-ctrl-test", "value"),
885             Input("cl-ctrl-core", "value"),
886             Input("cl-ctrl-core-all", "value"),
887             Input("cl-ctrl-framesize", "value"),
888             Input("cl-ctrl-framesize-all", "value"),
889             Input("cl-ctrl-testtype", "value"),
890             Input("cl-ctrl-testtype-all", "value"),
891             Input("cl-ctrl-normalize", "value"),
892             Input("btn-ctrl-add", "n_clicks"),
893             Input("dpr-period", "start_date"),
894             Input("dpr-period", "end_date"),
895             Input("btn-sel-remove", "n_clicks"),
896             Input("btn-sel-remove-all", "n_clicks"),
897             Input("url", "href")
898         )
899         def _update_ctrl_panel(cp_data: dict, store_sel: list, list_sel: list,
900             dd_dut: str, dd_phy: str, dd_area: str, dd_test: str, cl_core: list,
901             cl_core_all: list, cl_framesize: list, cl_framesize_all: list,
902             cl_testtype: list, cl_testtype_all: list, cl_normalize: list,
903             btn_add: int, d_start: str, d_end: str, btn_remove: int,
904             btn_remove_all: int, href: str) -> tuple:
905             """Update the application when the event is detected.
906
907             :param cp_data: Current status of the control panel stored in
908                 browser.
909             :param store_sel: List of tests selected by user stored in the
910                 browser.
911             :param list_sel: List of tests selected by the user shown in the
912                 checklist.
913             :param dd_dut: Input - DUTs.
914             :param dd_phy: Input - topo- arch-nic-driver.
915             :param dd_area: Input - Tested area.
916             :param dd_test: Input - Test.
917             :param cl_core: Input - Number of cores.
918             :param cl_core_all: Input - All numbers of cores.
919             :param cl_framesize: Input - Frame sizes.
920             :param cl_framesize_all: Input - All frame sizes.
921             :param cl_testtype: Input - Test type (NDR, PDR, MRR).
922             :param cl_testtype_all: Input - All test types.
923             :param cl_normalize: Input - Normalize the results.
924             :param btn_add: Input - Button "Add Selected" tests.
925             :param d_start: Date and time where the data processing starts.
926             :param d_end: Date and time where the data processing ends.
927             :param btn_remove: Input - Button "Remove selected" tests.
928             :param btn_remove_all: Input - Button "Remove All" tests.
929             :param href: Input - The URL provided by the browser.
930             :type cp_data: dict
931             :type store_sel: list
932             :type list_sel: list
933             :type dd_dut: str
934             :type dd_phy: str
935             :type dd_area: str
936             :type dd_test: str
937             :type cl_core: list
938             :type cl_core_all: list
939             :type cl_framesize: list
940             :type cl_framesize_all: list
941             :type cl_testtype: list
942             :type cl_testtype_all: list
943             :type cl_normalize: list
944             :type btn_add: int
945             :type d_start: str
946             :type d_end: str
947             :type btn_remove: int
948             :type btn_remove_all: int
949             :type href: str
950             :returns: New values for web page elements.
951             :rtype: tuple
952             """
953
954             ctrl_panel = self.ControlPanel(cp_data)
955             norm = cl_normalize
956
957             d_start = get_date(d_start)
958             d_end = get_date(d_end)
959
960             # Parse the url:
961             parsed_url = url_decode(href)
962             if parsed_url:
963                 url_params = parsed_url["params"]
964             else:
965                 url_params = None
966
967             row_fig_tput = no_update
968             row_fig_lat = no_update
969             row_btn_dwnld = no_update
970             row_card_sel_tests = no_update
971             row_btns_sel_tests = no_update
972
973             trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
974
975             if trigger_id == "dd-ctrl-dut":
976                 try:
977                     options = \
978                         generate_options(sorted(self.spec_tbs[dd_dut].keys()))
979                     disabled = False
980                 except KeyError:
981                     options = list()
982                     disabled = True
983                 ctrl_panel.set({
984                     "dd-ctrl-dut-value": dd_dut,
985                     "dd-ctrl-phy-value": str(),
986                     "dd-ctrl-phy-options": options,
987                     "dd-ctrl-phy-disabled": disabled,
988                     "dd-ctrl-area-value": str(),
989                     "dd-ctrl-area-options": list(),
990                     "dd-ctrl-area-disabled": True,
991                     "dd-ctrl-test-value": str(),
992                     "dd-ctrl-test-options": list(),
993                     "dd-ctrl-test-disabled": True,
994                     "cl-ctrl-core-options": list(),
995                     "cl-ctrl-core-value": list(),
996                     "cl-ctrl-core-all-value": list(),
997                     "cl-ctrl-core-all-options": C.CL_ALL_DISABLED,
998                     "cl-ctrl-framesize-options": list(),
999                     "cl-ctrl-framesize-value": list(),
1000                     "cl-ctrl-framesize-all-value": list(),
1001                     "cl-ctrl-framesize-all-options": C.CL_ALL_DISABLED,
1002                     "cl-ctrl-testtype-options": list(),
1003                     "cl-ctrl-testtype-value": list(),
1004                     "cl-ctrl-testtype-all-value": list(),
1005                     "cl-ctrl-testtype-all-options": C.CL_ALL_DISABLED,
1006                 })
1007             elif trigger_id == "dd-ctrl-phy":
1008                 try:
1009                     dut = ctrl_panel.get("dd-ctrl-dut-value")
1010                     phy = self.spec_tbs[dut][dd_phy]
1011                     options = [{"label": label(v), "value": v} \
1012                         for v in sorted(phy.keys())]
1013                     disabled = False
1014                 except KeyError:
1015                     options = list()
1016                     disabled = True
1017                 ctrl_panel.set({
1018                     "dd-ctrl-phy-value": dd_phy,
1019                     "dd-ctrl-area-value": str(),
1020                     "dd-ctrl-area-options": options,
1021                     "dd-ctrl-area-disabled": disabled,
1022                     "dd-ctrl-test-value": str(),
1023                     "dd-ctrl-test-options": list(),
1024                     "dd-ctrl-test-disabled": True,
1025                     "cl-ctrl-core-options": list(),
1026                     "cl-ctrl-core-value": list(),
1027                     "cl-ctrl-core-all-value": list(),
1028                     "cl-ctrl-core-all-options": C.CL_ALL_DISABLED,
1029                     "cl-ctrl-framesize-options": list(),
1030                     "cl-ctrl-framesize-value": list(),
1031                     "cl-ctrl-framesize-all-value": list(),
1032                     "cl-ctrl-framesize-all-options": C.CL_ALL_DISABLED,
1033                     "cl-ctrl-testtype-options": list(),
1034                     "cl-ctrl-testtype-value": list(),
1035                     "cl-ctrl-testtype-all-value": list(),
1036                     "cl-ctrl-testtype-all-options": C.CL_ALL_DISABLED,
1037                 })
1038             elif trigger_id == "dd-ctrl-area":
1039                 try:
1040                     dut = ctrl_panel.get("dd-ctrl-dut-value")
1041                     phy = ctrl_panel.get("dd-ctrl-phy-value")
1042                     area = self.spec_tbs[dut][phy][dd_area]
1043                     options = generate_options(sorted(area.keys()))
1044                     disabled = False
1045                 except KeyError:
1046                     options = list()
1047                     disabled = True
1048                 ctrl_panel.set({
1049                     "dd-ctrl-area-value": dd_area,
1050                     "dd-ctrl-test-value": str(),
1051                     "dd-ctrl-test-options": options,
1052                     "dd-ctrl-test-disabled": disabled,
1053                     "cl-ctrl-core-options": list(),
1054                     "cl-ctrl-core-value": list(),
1055                     "cl-ctrl-core-all-value": list(),
1056                     "cl-ctrl-core-all-options": C.CL_ALL_DISABLED,
1057                     "cl-ctrl-framesize-options": list(),
1058                     "cl-ctrl-framesize-value": list(),
1059                     "cl-ctrl-framesize-all-value": list(),
1060                     "cl-ctrl-framesize-all-options": C.CL_ALL_DISABLED,
1061                     "cl-ctrl-testtype-options": list(),
1062                     "cl-ctrl-testtype-value": list(),
1063                     "cl-ctrl-testtype-all-value": list(),
1064                     "cl-ctrl-testtype-all-options": C.CL_ALL_DISABLED,
1065                 })
1066             elif trigger_id == "dd-ctrl-test":
1067                 dut = ctrl_panel.get("dd-ctrl-dut-value")
1068                 phy = ctrl_panel.get("dd-ctrl-phy-value")
1069                 area = ctrl_panel.get("dd-ctrl-area-value")
1070                 if all((dut, phy, area, dd_test, )):
1071                     test = self.spec_tbs[dut][phy][area][dd_test]
1072                     ctrl_panel.set({
1073                         "dd-ctrl-test-value": dd_test,
1074                         "cl-ctrl-core-options": \
1075                             generate_options(sorted(test["core"])),
1076                         "cl-ctrl-core-value": list(),
1077                         "cl-ctrl-core-all-value": list(),
1078                         "cl-ctrl-core-all-options": C.CL_ALL_ENABLED,
1079                         "cl-ctrl-framesize-options": \
1080                             generate_options(sorted(test["frame-size"])),
1081                         "cl-ctrl-framesize-value": list(),
1082                         "cl-ctrl-framesize-all-value": list(),
1083                         "cl-ctrl-framesize-all-options": C.CL_ALL_ENABLED,
1084                         "cl-ctrl-testtype-options": \
1085                             generate_options(sorted(test["test-type"])),
1086                         "cl-ctrl-testtype-value": list(),
1087                         "cl-ctrl-testtype-all-value": list(),
1088                         "cl-ctrl-testtype-all-options": C.CL_ALL_ENABLED,
1089                     })
1090             elif trigger_id == "cl-ctrl-core":
1091                 val_sel, val_all = sync_checklists(
1092                     options=ctrl_panel.get("cl-ctrl-core-options"),
1093                     sel=cl_core,
1094                     all=list(),
1095                     id=""
1096                 )
1097                 ctrl_panel.set({
1098                     "cl-ctrl-core-value": val_sel,
1099                     "cl-ctrl-core-all-value": val_all,
1100                 })
1101             elif trigger_id == "cl-ctrl-core-all":
1102                 val_sel, val_all = sync_checklists(
1103                     options = ctrl_panel.get("cl-ctrl-core-options"),
1104                     sel=list(),
1105                     all=cl_core_all,
1106                     id="all"
1107                 )
1108                 ctrl_panel.set({
1109                     "cl-ctrl-core-value": val_sel,
1110                     "cl-ctrl-core-all-value": val_all,
1111                 })
1112             elif trigger_id == "cl-ctrl-framesize":
1113                 val_sel, val_all = sync_checklists(
1114                     options = ctrl_panel.get("cl-ctrl-framesize-options"),
1115                     sel=cl_framesize,
1116                     all=list(),
1117                     id=""
1118                 )
1119                 ctrl_panel.set({
1120                     "cl-ctrl-framesize-value": val_sel,
1121                     "cl-ctrl-framesize-all-value": val_all,
1122                 })
1123             elif trigger_id == "cl-ctrl-framesize-all":
1124                 val_sel, val_all = sync_checklists(
1125                     options = ctrl_panel.get("cl-ctrl-framesize-options"),
1126                     sel=list(),
1127                     all=cl_framesize_all,
1128                     id="all"
1129                 )
1130                 ctrl_panel.set({
1131                     "cl-ctrl-framesize-value": val_sel,
1132                     "cl-ctrl-framesize-all-value": val_all,
1133                 })
1134             elif trigger_id == "cl-ctrl-testtype":
1135                 val_sel, val_all = sync_checklists(
1136                     options = ctrl_panel.get("cl-ctrl-testtype-options"),
1137                     sel=cl_testtype,
1138                     all=list(),
1139                     id=""
1140                 )
1141                 ctrl_panel.set({
1142                     "cl-ctrl-testtype-value": val_sel,
1143                     "cl-ctrl-testtype-all-value": val_all,
1144                 })
1145             elif trigger_id == "cl-ctrl-testtype-all":
1146                 val_sel, val_all = sync_checklists(
1147                     options = ctrl_panel.get("cl-ctrl-testtype-options"),
1148                     sel=list(),
1149                     all=cl_testtype_all,
1150                     id="all"
1151                 )
1152                 ctrl_panel.set({
1153                     "cl-ctrl-testtype-value": val_sel,
1154                     "cl-ctrl-testtype-all-value": val_all,
1155                 })
1156             elif trigger_id == "btn-ctrl-add":
1157                 _ = btn_add
1158                 dut = ctrl_panel.get("dd-ctrl-dut-value")
1159                 phy = ctrl_panel.get("dd-ctrl-phy-value")
1160                 area = ctrl_panel.get("dd-ctrl-area-value")
1161                 test = ctrl_panel.get("dd-ctrl-test-value")
1162                 cores = ctrl_panel.get("cl-ctrl-core-value")
1163                 framesizes = ctrl_panel.get("cl-ctrl-framesize-value")
1164                 testtypes = ctrl_panel.get("cl-ctrl-testtype-value")
1165                 # Add selected test to the list of tests in store:
1166                 if all((dut, phy, area, test, cores, framesizes, testtypes)):
1167                     if store_sel is None:
1168                         store_sel = list()
1169                     for core in cores:
1170                         for framesize in framesizes:
1171                             for ttype in testtypes:
1172                                 if dut == "trex":
1173                                     core = str()
1174                                 tid = "-".join((
1175                                     dut, phy.replace('af_xdp', 'af-xdp'), area,
1176                                     framesize.lower(), core.lower(), test,
1177                                     ttype.lower()
1178                                 ))
1179                                 if tid not in [itm["id"] for itm in store_sel]:
1180                                     store_sel.append({
1181                                         "id": tid,
1182                                         "dut": dut,
1183                                         "phy": phy,
1184                                         "area": area,
1185                                         "test": test,
1186                                         "framesize": framesize.lower(),
1187                                         "core": core.lower(),
1188                                         "testtype": ttype.lower()
1189                                     })
1190                     store_sel = sorted(store_sel, key=lambda d: d["id"])
1191                     row_card_sel_tests = C.STYLE_ENABLED
1192                     row_btns_sel_tests = C.STYLE_ENABLED
1193                     if C.CLEAR_ALL_INPUTS:
1194                         ctrl_panel.set(ctrl_panel.defaults)
1195             elif trigger_id == "btn-sel-remove-all":
1196                 _ = btn_remove_all
1197                 row_fig_tput = C.PLACEHOLDER
1198                 row_fig_lat = C.PLACEHOLDER
1199                 row_btn_dwnld = C.PLACEHOLDER
1200                 row_card_sel_tests = C.STYLE_DISABLED
1201                 row_btns_sel_tests = C.STYLE_DISABLED
1202                 store_sel = list()
1203                 ctrl_panel.set({"cl-selected-options": list()})
1204             elif trigger_id == "btn-sel-remove":
1205                 _ = btn_remove
1206                 if list_sel:
1207                     new_store_sel = list()
1208                     for item in store_sel:
1209                         if item["id"] not in list_sel:
1210                             new_store_sel.append(item)
1211                     store_sel = new_store_sel
1212             elif trigger_id == "url":
1213                 if url_params:
1214                     try:
1215                         store_sel = literal_eval(url_params["store_sel"][0])
1216                         d_start = get_date(url_params["start"][0])
1217                         d_end = get_date(url_params["end"][0])
1218                         norm = literal_eval(url_params["norm"][0])
1219                     except (KeyError, IndexError):
1220                         pass
1221                     if store_sel:
1222                         row_card_sel_tests = C.STYLE_ENABLED
1223                         row_btns_sel_tests = C.STYLE_ENABLED
1224                         last_test = store_sel[-1]
1225                         test = self.spec_tbs[last_test["dut"]]\
1226                             [last_test["phy"]][last_test["area"]]\
1227                                 [last_test["test"]]
1228                         ctrl_panel.set({
1229                             "dd-ctrl-dut-value": last_test["dut"],
1230                             "dd-ctrl-phy-value": last_test["phy"],
1231                             "dd-ctrl-phy-options": generate_options(sorted(
1232                                 self.spec_tbs[last_test["dut"]].keys())),
1233                             "dd-ctrl-phy-disabled": False,
1234                             "dd-ctrl-area-value": last_test["area"],
1235                             "dd-ctrl-area-options": [
1236                                 {"label": label(v), "value": v} \
1237                                     for v in sorted(
1238                                         self.spec_tbs[last_test["dut"]]\
1239                                             [last_test["phy"]].keys())
1240                             ],
1241                             "dd-ctrl-area-disabled": False,
1242                             "dd-ctrl-test-value": last_test["test"],
1243                             "dd-ctrl-test-options": generate_options(sorted(
1244                                 self.spec_tbs[last_test["dut"]]\
1245                                     [last_test["phy"]]\
1246                                         [last_test["area"]].keys())),
1247                             "dd-ctrl-test-disabled": False,
1248                             "cl-ctrl-core-options": generate_options(sorted(
1249                                 test["core"])),
1250                             "cl-ctrl-core-value": [last_test["core"].upper(), ],
1251                             "cl-ctrl-core-all-value": list(),
1252                             "cl-ctrl-core-all-options": C.CL_ALL_ENABLED,
1253                             "cl-ctrl-framesize-options": generate_options(
1254                                 sorted(test["frame-size"])),
1255                             "cl-ctrl-framesize-value": \
1256                                 [last_test["framesize"].upper(), ],
1257                             "cl-ctrl-framesize-all-value": list(),
1258                             "cl-ctrl-framesize-all-options": C.CL_ALL_ENABLED,
1259                             "cl-ctrl-testtype-options": generate_options(sorted(
1260                                 test["test-type"])),
1261                             "cl-ctrl-testtype-value": \
1262                                 [last_test["testtype"].upper(), ],
1263                             "cl-ctrl-testtype-all-value": list(),
1264                             "cl-ctrl-testtype-all-options": C.CL_ALL_ENABLED
1265                         })
1266
1267             if trigger_id in ("btn-ctrl-add", "url", "dpr-period",
1268                     "btn-sel-remove", "cl-ctrl-normalize"):
1269                 if store_sel:
1270                     row_fig_tput, row_fig_lat, row_btn_dwnld = \
1271                         _generate_plotting_area(
1272                             graph_trending(self.data, store_sel, self.layout,
1273                                 d_start, d_end, bool(norm)),
1274                             gen_new_url(
1275                                 parsed_url,
1276                                 {
1277                                     "store_sel": store_sel,
1278                                     "start": d_start,
1279                                     "end": d_end,
1280                                     "norm": norm
1281                                 }
1282                             )
1283                         )
1284                     ctrl_panel.set({
1285                         "cl-selected-options": list_tests(store_sel)
1286                     })
1287                 else:
1288                     row_fig_tput = C.PLACEHOLDER
1289                     row_fig_lat = C.PLACEHOLDER
1290                     row_btn_dwnld = C.PLACEHOLDER
1291                     row_card_sel_tests = C.STYLE_DISABLED
1292                     row_btns_sel_tests = C.STYLE_DISABLED
1293                     store_sel = list()
1294                     ctrl_panel.set({"cl-selected-options": list()})
1295
1296             if ctrl_panel.get("cl-ctrl-core-value") and \
1297                     ctrl_panel.get("cl-ctrl-framesize-value") and \
1298                     ctrl_panel.get("cl-ctrl-testtype-value"):
1299                 disabled = False
1300             else:
1301                 disabled = True
1302             ctrl_panel.set({
1303                 "btn-ctrl-add-disabled": disabled,
1304                 "cl-normalize-value": norm
1305             })
1306
1307             ret_val = [
1308                 ctrl_panel.panel, store_sel,
1309                 row_fig_tput, row_fig_lat, row_btn_dwnld,
1310                 row_card_sel_tests, row_btns_sel_tests
1311             ]
1312             ret_val.extend(ctrl_panel.values())
1313             return ret_val
1314
1315         @app.callback(
1316             Output("metadata-tput-lat", "children"),
1317             Output("metadata-hdrh-graph", "children"),
1318             Output("offcanvas-metadata", "is_open"),
1319             Input({"type": "graph", "index": ALL}, "clickData"),
1320             prevent_initial_call=True
1321         )
1322         def _show_metadata_from_graphs(graph_data: dict) -> tuple:
1323             """Generates the data for the offcanvas displayed when a particular
1324             point in a graph is clicked on.
1325
1326             :param graph_data: The data from the clicked point in the graph.
1327             :type graph_data: dict
1328             :returns: The data to be displayed on the offcanvas and the
1329                 information to show the offcanvas.
1330             :rtype: tuple(list, list, bool)
1331             """
1332             try:
1333                 trigger_id = loads(
1334                     callback_context.triggered[0]["prop_id"].split(".")[0]
1335                 )["index"]
1336                 idx = 0 if trigger_id == "tput" else 1
1337                 graph_data = graph_data[idx]["points"][0]
1338             except (JSONDecodeError, IndexError, KeyError, ValueError,
1339                     TypeError):
1340                 raise PreventUpdate
1341
1342             metadata = no_update
1343             graph = list()
1344
1345             children = [
1346                 dbc.ListGroupItem(
1347                     [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
1348                 ) for x in graph_data.get("text", "").split("<br>")
1349             ]
1350             if trigger_id == "tput":
1351                 title = "Throughput"
1352             elif trigger_id == "lat":
1353                 title = "Latency"
1354                 hdrh_data = graph_data.get("customdata", None)
1355                 if hdrh_data:
1356                     graph = [dbc.Card(
1357                         class_name="gy-2 p-0",
1358                         children=[
1359                             dbc.CardHeader(hdrh_data.pop("name")),
1360                             dbc.CardBody(children=[
1361                                 dcc.Graph(
1362                                     id="hdrh-latency-graph",
1363                                     figure=graph_hdrh_latency(
1364                                         hdrh_data, self.layout
1365                                     )
1366                                 )
1367                             ])
1368                         ])
1369                     ]
1370             metadata = [
1371                 dbc.Card(
1372                     class_name="gy-2 p-0",
1373                     children=[
1374                         dbc.CardHeader(children=[
1375                             dcc.Clipboard(
1376                                 target_id="tput-lat-metadata",
1377                                 title="Copy",
1378                                 style={"display": "inline-block"}
1379                             ),
1380                             title
1381                         ]),
1382                         dbc.CardBody(
1383                             id="tput-lat-metadata",
1384                             class_name="p-0",
1385                             children=[dbc.ListGroup(children, flush=True), ]
1386                         )
1387                     ]
1388                 )
1389             ]
1390
1391             return metadata, graph, True
1392
1393         @app.callback(
1394             Output("download-data", "data"),
1395             State("selected-tests", "data"),
1396             Input("btn-download-data", "n_clicks"),
1397             prevent_initial_call=True
1398         )
1399         def _download_data(store_sel, n_clicks):
1400             """Download the data
1401
1402             :param store_sel: List of tests selected by user stored in the
1403                 browser.
1404             :param n_clicks: Number of clicks on the button "Download".
1405             :type store_sel: list
1406             :type n_clicks: int
1407             :returns: dict of data frame content (base64 encoded) and meta data
1408                 used by the Download component.
1409             :rtype: dict
1410             """
1411
1412             if not n_clicks:
1413                 raise PreventUpdate
1414
1415             if not store_sel:
1416                 raise PreventUpdate
1417
1418             df = pd.DataFrame()
1419             for itm in store_sel:
1420                 sel_data = select_trending_data(self.data, itm)
1421                 if sel_data is None:
1422                     continue
1423                 df = pd.concat([df, sel_data], ignore_index=True)
1424
1425             return dcc.send_data_frame(df.to_csv, C.TREND_DOWNLOAD_FILE_NAME)