C-Dash: Add search in tests
[csit.git] / csit.infra.dash / app / cdash / comparisons / layout.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 """Plotly Dash HTML layout override.
15 """
16
17 import pandas as pd
18 import dash_bootstrap_components as dbc
19
20 from flask import Flask
21 from dash import dcc, html, dash_table, callback_context, no_update, ALL
22 from dash import Input, Output, State
23 from dash.exceptions import PreventUpdate
24 from dash.dash_table.Format import Format, Scheme
25 from ast import literal_eval
26
27 from ..utils.constants import Constants as C
28 from ..utils.control_panel import ControlPanel
29 from ..utils.trigger import Trigger
30 from ..utils.url_processing import url_decode
31 from ..utils.utils import generate_options, gen_new_url, navbar_report, \
32     filter_table_data
33 from .tables import comparison_table
34
35
36 # Control panel partameters and their default values.
37 CP_PARAMS = {
38     "dut-val": str(),
39     "dutver-opt": list(),
40     "dutver-dis": True,
41     "dutver-val": str(),
42     "infra-opt": list(),
43     "infra-dis": True,
44     "infra-val": str(),
45     "core-opt": list(),
46     "core-val": list(),
47     "frmsize-opt": list(),
48     "frmsize-val": list(),
49     "ttype-opt": list(),
50     "ttype-val": list(),
51     "cmp-par-opt": list(),
52     "cmp-par-dis": True,
53     "cmp-par-val": str(),
54     "cmp-val-opt": list(),
55     "cmp-val-dis": True,
56     "cmp-val-val": str(),
57     "normalize-val": list()
58 }
59
60 # List of comparable parameters.
61 CMP_PARAMS = {
62     "dutver": "Release and Version",
63     "infra": "Infrastructure",
64     "frmsize": "Frame Size",
65     "core": "Number of Cores",
66     "ttype": "Measurement"
67 }
68
69
70 class Layout:
71     """The layout of the dash app and the callbacks.
72     """
73
74     def __init__(
75             self,
76             app: Flask,
77             data_iterative: pd.DataFrame,
78             html_layout_file: str
79         ) -> None:
80         """Initialization:
81         - save the input parameters,
82         - prepare data for the control panel,
83         - read HTML layout file,
84
85         :param app: Flask application running the dash application.
86         :param data_iterative: Iterative data to be used in comparison tables.
87         :param html_layout_file: Path and name of the file specifying the HTML
88             layout of the dash application.
89         :type app: Flask
90         :type data_iterative: pandas.DataFrame
91         :type html_layout_file: str
92         """
93
94         # Inputs
95         self._app = app
96         self._html_layout_file = html_layout_file
97         self._data = data_iterative
98
99         # Get structure of tests:
100         tbs = dict()
101         cols = [
102             "job", "test_id", "test_type", "dut_type", "dut_version", "tg_type",
103             "release", "passed"
104         ]
105         for _, row in self._data[cols].drop_duplicates().iterrows():
106             lst_job = row["job"].split("-")
107             dut = lst_job[1]
108             dver = f"{row['release']}-{row['dut_version']}"
109             tbed = "-".join(lst_job[-2:])
110             lst_test_id = row["test_id"].split(".")
111
112             suite = lst_test_id[-2].replace("2n1l-", "").replace("1n1l-", "").\
113                 replace("2n-", "")
114             test = lst_test_id[-1]
115             nic = suite.split("-")[0]
116             for driver in C.DRIVERS:
117                 if driver in test:
118                     drv = driver.replace("-", "_")
119                     test = test.replace(f"{driver}-", "")
120                     break
121             else:
122                 drv = "dpdk"
123             infra = "-".join((tbed, nic, drv))
124             lst_test = test.split("-")
125             fsize = lst_test[0]
126             core = lst_test[1] if lst_test[1] else "8C"
127
128             if tbs.get(dut, None) is None:
129                 tbs[dut] = dict()
130             if tbs[dut].get(dver, None) is None:
131                 tbs[dut][dver] = dict()
132             if tbs[dut][dver].get(infra, None) is None:
133                 tbs[dut][dver][infra] = dict()
134                 tbs[dut][dver][infra]["core"] = list()
135                 tbs[dut][dver][infra]["fsize"] = list()
136                 tbs[dut][dver][infra]["ttype"] = list()
137             if core.upper() not in tbs[dut][dver][infra]["core"]:
138                 tbs[dut][dver][infra]["core"].append(core.upper())
139             if fsize.upper() not in tbs[dut][dver][infra]["fsize"]:
140                 tbs[dut][dver][infra]["fsize"].append(fsize.upper())
141             if row["test_type"] == "mrr":
142                 if "MRR" not in tbs[dut][dver][infra]["ttype"]:
143                     tbs[dut][dver][infra]["ttype"].append("MRR")
144             elif row["test_type"] == "ndrpdr":
145                 if "NDR" not in tbs[dut][dver][infra]["ttype"]:
146                     tbs[dut][dver][infra]["ttype"].extend(
147                         ("NDR", "PDR", "Latency")
148                     )
149             elif row["test_type"] == "hoststack" and \
150                     row["tg_type"] in ("iperf", "vpp"):
151                 if "BPS" not in tbs[dut][dver][infra]["ttype"]:
152                     tbs[dut][dver][infra]["ttype"].append("BPS")
153             elif row["test_type"] == "hoststack" and row["tg_type"] == "ab":
154                 if "CPS" not in tbs[dut][dver][infra]["ttype"]:
155                     tbs[dut][dver][infra]["ttype"].extend(("CPS", "RPS", ))
156         self._tbs = tbs
157
158         # Read from files:
159         self._html_layout = str()
160         try:
161             with open(self._html_layout_file, "r") as file_read:
162                 self._html_layout = file_read.read()
163         except IOError as err:
164             raise RuntimeError(
165                 f"Not possible to open the file {self._html_layout_file}\n{err}"
166             )
167
168         # Callbacks:
169         if self._app is not None and hasattr(self, "callbacks"):
170             self.callbacks(self._app)
171
172     @property
173     def html_layout(self):
174         return self._html_layout
175
176     def add_content(self):
177         """Top level method which generated the web page.
178
179         It generates:
180         - Store for user input data,
181         - Navigation bar,
182         - Main area with control panel and ploting area.
183
184         If no HTML layout is provided, an error message is displayed instead.
185
186         :returns: The HTML div with the whole page.
187         :rtype: html.Div
188         """
189
190         if self.html_layout and self._tbs:
191             return html.Div(
192                 id="div-main",
193                 className="small",
194                 children=[
195                     dbc.Row(
196                         id="row-navbar",
197                         class_name="g-0",
198                         children=[navbar_report((False, True, False, False)), ]
199                     ),
200                     dbc.Row(
201                         id="row-main",
202                         class_name="g-0",
203                         children=[
204                             dcc.Store(id="store-control-panel"),
205                             dcc.Store(id="store-selected"),
206                             dcc.Store(id="store-table-data"),
207                             dcc.Store(id="store-filtered-table-data"),
208                             dcc.Location(id="url", refresh=False),
209                             self._add_ctrl_col(),
210                             self._add_plotting_col()
211                         ]
212                     ),
213                     dbc.Offcanvas(
214                         class_name="w-75",
215                         id="offcanvas-documentation",
216                         title="Documentation",
217                         placement="end",
218                         is_open=False,
219                         children=html.Iframe(
220                             src=C.URL_DOC_REL_NOTES,
221                             width="100%",
222                             height="100%"
223                         )
224                     )
225                 ]
226             )
227         else:
228             return html.Div(
229                 id="div-main-error",
230                 children=[
231                     dbc.Alert(
232                         [
233                             "An Error Occured"
234                         ],
235                         color="danger"
236                     )
237                 ]
238             )
239
240     def _add_ctrl_col(self) -> dbc.Col:
241         """Add column with controls. It is placed on the left side.
242
243         :returns: Column with the control panel.
244         :rtype: dbc.Col
245         """
246         return dbc.Col([
247             html.Div(
248                 children=self._add_ctrl_panel(),
249                 className="sticky-top"
250             )
251         ])
252
253     def _add_plotting_col(self) -> dbc.Col:
254         """Add column with plots. It is placed on the right side.
255
256         :returns: Column with plots.
257         :rtype: dbc.Col
258         """
259         return dbc.Col(
260             id="col-plotting-area",
261             children=[
262                 dbc.Spinner(
263                     children=[
264                         dbc.Row(
265                             id="plotting-area",
266                             class_name="g-0 p-0",
267                             children=[
268                                 C.PLACEHOLDER
269                             ]
270                         )
271                     ]
272                 )
273             ],
274             width=9
275         )
276
277     def _add_ctrl_panel(self) -> list:
278         """Add control panel.
279
280         :returns: Control panel.
281         :rtype: list
282         """
283
284         reference = [
285             dbc.Row(
286                 class_name="g-0 p-1",
287                 children=[
288                     dbc.InputGroup(
289                         [
290                             dbc.InputGroupText("DUT"),
291                             dbc.Select(
292                                 id={"type": "ctrl-dd", "index": "dut"},
293                                 placeholder="Select a Device under Test...",
294                                 options=sorted(
295                                     [
296                                         {"label": k, "value": k} \
297                                             for k in self._tbs.keys()
298                                     ],
299                                     key=lambda d: d["label"]
300                                 )
301                             )
302                         ],
303                         size="sm"
304                     )
305                 ]
306             ),
307             dbc.Row(
308                 class_name="g-0 p-1",
309                 children=[
310                     dbc.InputGroup(
311                         [
312                             dbc.InputGroupText("CSIT and DUT Version"),
313                             dbc.Select(
314                                 id={"type": "ctrl-dd", "index": "dutver"},
315                                 placeholder="Select a CSIT and DUT Version...")
316                         ],
317                         size="sm"
318                     )
319                 ]
320             ),
321             dbc.Row(
322                 class_name="g-0 p-1",
323                 children=[
324                     dbc.InputGroup(
325                         [
326                             dbc.InputGroupText("Infra"),
327                             dbc.Select(
328                                 id={"type": "ctrl-dd", "index": "infra"},
329                                 placeholder=\
330                                     "Select a Physical Test Bed Topology..."
331                             )
332                         ],
333                         size="sm"
334                     )
335                 ]
336             ),
337             dbc.Row(
338                 class_name="g-0 p-1",
339                 children=[
340                     dbc.InputGroup(
341                         [
342                             dbc.InputGroupText("Frame Size"),
343                             dbc.Checklist(
344                                 id={"type": "ctrl-cl", "index": "frmsize"},
345                                 inline=True,
346                                 class_name="ms-2"
347                             )
348                         ],
349                         style={"align-items": "center"},
350                         size="sm"
351                     )
352                 ]
353             ),
354             dbc.Row(
355                 class_name="g-0 p-1",
356                 children=[
357                     dbc.InputGroup(
358                         [
359                             dbc.InputGroupText("Number of Cores"),
360                             dbc.Checklist(
361                                 id={"type": "ctrl-cl", "index": "core"},
362                                 inline=True,
363                                 class_name="ms-2"
364                             )
365                         ],
366                         style={"align-items": "center"},
367                         size="sm"
368                     )
369                 ]
370             ),
371             dbc.Row(
372                 class_name="g-0 p-1",
373                 children=[
374                     dbc.InputGroup(
375                         [
376                             dbc.InputGroupText("Measurement"),
377                             dbc.Checklist(
378                                 id={"type": "ctrl-cl", "index": "ttype"},
379                                 inline=True,
380                                 class_name="ms-2"
381                             )
382                         ],
383                         style={"align-items": "center"},
384                         size="sm"
385                     )
386                 ]
387             )
388         ]
389
390         compare = [
391             dbc.Row(
392                 class_name="g-0 p-1",
393                 children=[
394                     dbc.InputGroup(
395                         [
396                             dbc.InputGroupText("Parameter"),
397                             dbc.Select(
398                                 id={"type": "ctrl-dd", "index": "cmpprm"},
399                                 placeholder="Select a Parameter..."
400                             )
401                         ],
402                         size="sm"
403                     )
404                 ]
405             ),
406             dbc.Row(
407                 class_name="g-0 p-1",
408                 children=[
409                     dbc.InputGroup(
410                         [
411                             dbc.InputGroupText("Value"),
412                             dbc.Select(
413                                 id={"type": "ctrl-dd", "index": "cmpval"},
414                                 placeholder="Select a Value..."
415                             )
416                         ],
417                         size="sm"
418                     )
419                 ]
420             )
421         ]
422
423         normalize = [
424             dbc.Row(
425                 class_name="g-0 p-1",
426                 children=[
427                     dbc.InputGroup(
428                         dbc.Checklist(
429                             id="normalize",
430                             options=[{
431                                 "value": "normalize",
432                                 "label": "Normalize to 2GHz CPU frequency"
433                             }],
434                             value=[],
435                             inline=True,
436                             class_name="ms-2"
437                         ),
438                         style={"align-items": "center"},
439                         size="sm"
440                     )
441                 ]
442             )
443         ]
444
445         return [
446             dbc.Row(
447                 dbc.Card(
448                     [
449                         dbc.CardHeader(
450                             html.H5("Reference Value")
451                         ),
452                         dbc.CardBody(
453                             children=reference,
454                             class_name="g-0 p-0"
455                         )
456                     ],
457                     color="secondary",
458                     outline=True
459                 ),
460                 class_name="g-0 p-1"
461             ),
462             dbc.Row(
463                 dbc.Card(
464                     [
465                         dbc.CardHeader(
466                             html.H5("Compared Value")
467                         ),
468                         dbc.CardBody(
469                             children=compare,
470                             class_name="g-0 p-0"
471                         )
472                     ],
473                     color="secondary",
474                     outline=True
475                 ),
476                 class_name="g-0 p-1"
477             ),
478             dbc.Row(
479                 dbc.Card(
480                     [
481                         dbc.CardHeader(
482                             html.H5("Normalization")
483                         ),
484                         dbc.CardBody(
485                             children=normalize,
486                             class_name="g-0 p-0"
487                         )
488                     ],
489                     color="secondary",
490                     outline=True
491                 ),
492                 class_name="g-0 p-1"
493             )
494         ]
495
496     @staticmethod
497     def _get_plotting_area(
498             title: str,
499             table: pd.DataFrame,
500             url: str
501         ) -> list:
502         """Generate the plotting area with all its content.
503
504         :param title: The title of the comparison table.
505         :param table: Comparison table to be displayed.
506         :param url: URL to be displayed in the modal window.
507         :type title: str
508         :type table: pandas.DataFrame
509         :type url: str
510         :returns: List of rows with elements to be displayed in the plotting
511             area.
512         :rtype: list
513         """
514
515         if table.empty:
516             return dbc.Row(
517                 dbc.Col(
518                     children=dbc.Alert(
519                         "No data for comparison.",
520                         color="danger"
521                     ),
522                     class_name="g-0 p-1",
523                 ),
524                 class_name="g-0 p-0"
525             )
526
527         cols = list()
528         for idx, col in enumerate(table.columns):
529             if idx == 0:
530                 cols.append({
531                     "name": ["", col],
532                     "id": col,
533                     "deletable": False,
534                     "selectable": False,
535                     "type": "text"
536                 })
537             else:
538                 l_col = col.rsplit(" ", 2)
539                 cols.append({
540                     "name": [l_col[0], " ".join(l_col[-2:])],
541                     "id": col,
542                     "deletable": False,
543                     "selectable": False,
544                     "type": "numeric",
545                     "format": Format(precision=2, scheme=Scheme.fixed)
546                 })
547
548         return [
549             dbc.Row(
550                 children=html.H5(title),
551                 class_name="g-0 p-1"
552             ),
553             dbc.Row(
554                 children=[
555                     dbc.Col(
556                         children=dash_table.DataTable(
557                             id={"type": "table", "index": "comparison"},
558                             columns=cols,
559                             data=table.to_dict("records"),
560                             merge_duplicate_headers=True,
561                             editable=False,
562                             filter_action="custom",
563                             filter_query="",
564                             sort_action="native",
565                             sort_mode="multi",
566                             selected_columns=[],
567                             selected_rows=[],
568                             page_action="none",
569                             style_cell={"textAlign": "right"},
570                             style_cell_conditional=[{
571                                 "if": {"column_id": "Test Name"},
572                                 "textAlign": "left"
573                             }]
574                         ),
575                         class_name="g-0 p-1"
576                     )
577                 ],
578                 class_name="g-0 p-0"
579             ),
580             dbc.Row(
581                 [
582                     dbc.Col([html.Div(
583                         [
584                             dbc.Button(
585                                 id="plot-btn-url",
586                                 children="Show URL",
587                                 class_name="me-1",
588                                 color="info",
589                                 style={
590                                     "text-transform": "none",
591                                     "padding": "0rem 1rem"
592                                 }
593                             ),
594                             dbc.Modal(
595                                 [
596                                     dbc.ModalHeader(dbc.ModalTitle("URL")),
597                                     dbc.ModalBody(url)
598                                 ],
599                                 id="plot-mod-url",
600                                 size="xl",
601                                 is_open=False,
602                                 scrollable=True
603                             ),
604                             dbc.Button(
605                                 id="plot-btn-download",
606                                 children="Download Data",
607                                 class_name="me-1",
608                                 color="info",
609                                 style={
610                                     "text-transform": "none",
611                                     "padding": "0rem 1rem"
612                                 }
613                             ),
614                             dcc.Download(id="download-iterative-data")
615                         ],
616                         className=\
617                             "d-grid gap-0 d-md-flex justify-content-md-end"
618                     )])
619                 ],
620                 class_name="g-0 p-0"
621             ),
622             dbc.Row(
623                 children=C.PLACEHOLDER,
624                 class_name="g-0 p-1"
625             )
626         ]
627
628     def callbacks(self, app):
629         """Callbacks for the whole application.
630
631         :param app: The application.
632         :type app: Flask
633         """
634
635         @app.callback(
636             [
637                 Output("store-control-panel", "data"),
638                 Output("store-selected", "data"),
639                 Output("store-table-data", "data"),
640                 Output("store-filtered-table-data", "data"),
641                 Output("plotting-area", "children"),
642                 Output({"type": "table", "index": ALL}, "data"),
643                 Output({"type": "ctrl-dd", "index": "dut"}, "value"),
644                 Output({"type": "ctrl-dd", "index": "dutver"}, "options"),
645                 Output({"type": "ctrl-dd", "index": "dutver"}, "disabled"),
646                 Output({"type": "ctrl-dd", "index": "dutver"}, "value"),
647                 Output({"type": "ctrl-dd", "index": "infra"}, "options"),
648                 Output({"type": "ctrl-dd", "index": "infra"}, "disabled"),
649                 Output({"type": "ctrl-dd", "index": "infra"}, "value"),
650                 Output({"type": "ctrl-cl", "index": "core"}, "options"),
651                 Output({"type": "ctrl-cl", "index": "core"}, "value"),
652                 Output({"type": "ctrl-cl", "index": "frmsize"}, "options"),
653                 Output({"type": "ctrl-cl", "index": "frmsize"}, "value"),
654                 Output({"type": "ctrl-cl", "index": "ttype"}, "options"),
655                 Output({"type": "ctrl-cl", "index": "ttype"}, "value"),
656                 Output({"type": "ctrl-dd", "index": "cmpprm"}, "options"),
657                 Output({"type": "ctrl-dd", "index": "cmpprm"}, "disabled"),
658                 Output({"type": "ctrl-dd", "index": "cmpprm"}, "value"),
659                 Output({"type": "ctrl-dd", "index": "cmpval"}, "options"),
660                 Output({"type": "ctrl-dd", "index": "cmpval"}, "disabled"),
661                 Output({"type": "ctrl-dd", "index": "cmpval"}, "value"),
662                 Output("normalize", "value")
663             ],
664             [
665                 State("store-control-panel", "data"),
666                 State("store-selected", "data"),
667                 State("store-table-data", "data"),
668                 State("store-filtered-table-data", "data"),
669                 State({"type": "table", "index": ALL}, "data")
670             ],
671             [
672                 Input("url", "href"),
673                 Input("normalize", "value"),
674                 Input({"type": "table", "index": ALL}, "filter_query"),
675                 Input({"type": "ctrl-dd", "index": ALL}, "value"),
676                 Input({"type": "ctrl-cl", "index": ALL}, "value"),
677                 Input({"type": "ctrl-btn", "index": ALL}, "n_clicks")
678             ]
679         )
680         def _update_application(
681                 control_panel: dict,
682                 selected: dict,
683                 store_table_data: list,
684                 filtered_data: list,
685                 table_data: list,
686                 href: str,
687                 normalize: list,
688                 table_filter: str,
689                 *_
690             ) -> tuple:
691             """Update the application when the event is detected.
692             """
693
694             ctrl_panel = ControlPanel(CP_PARAMS, control_panel)
695
696             if selected is None:
697                 selected = {
698                     "reference": {
699                         "set": False,
700                     },
701                     "compare": {
702                         "set": False,
703                     }
704                 }
705
706             # Parse the url:
707             parsed_url = url_decode(href)
708             if parsed_url:
709                 url_params = parsed_url["params"]
710             else:
711                 url_params = None
712
713             on_draw = False
714             plotting_area = no_update
715
716             trigger = Trigger(callback_context.triggered)
717             if trigger.type == "url" and url_params:
718                 process_url = False
719                 try:
720                     selected = literal_eval(url_params["selected"][0])
721                     r_sel = selected["reference"]["selection"]
722                     c_sel = selected["compare"]
723                     normalize = literal_eval(url_params["norm"][0])
724                     process_url = bool(
725                         (selected["reference"]["set"] == True) and
726                         (c_sel["set"] == True)
727                     )
728                 except (KeyError, IndexError, AttributeError):
729                     pass
730                 if process_url:
731                     ctrl_panel.set({
732                         "dut-val": r_sel["dut"],
733                         "dutver-opt": generate_options(
734                             self._tbs[r_sel["dut"]].keys()
735                         ),
736                         "dutver-dis": False,
737                         "dutver-val": r_sel["dutver"],
738                         "infra-opt": generate_options(
739                             self._tbs[r_sel["dut"]][r_sel["dutver"]].keys()
740                         ),
741                         "infra-dis": False,
742                         "infra-val": r_sel["infra"],
743                         "core-opt": generate_options(
744                             self._tbs[r_sel["dut"]][r_sel["dutver"]]\
745                                 [r_sel["infra"]]["core"]
746                         ),
747                         "core-val": r_sel["core"],
748                         "frmsize-opt": generate_options(
749                             self._tbs[r_sel["dut"]][r_sel["dutver"]]\
750                                 [r_sel["infra"]]["fsize"]
751                         ),
752                         "frmsize-val": r_sel["frmsize"],
753                         "ttype-opt": generate_options(
754                             self._tbs[r_sel["dut"]][r_sel["dutver"]]\
755                                 [r_sel["infra"]]["ttype"]
756                         ),
757                         "ttype-val": r_sel["ttype"],
758                         "normalize-val": normalize
759                     })
760                     opts = list()
761                     for itm, label in CMP_PARAMS.items():
762                         if len(ctrl_panel.get(f"{itm}-opt")) > 1:
763                             opts.append({"label": label, "value": itm})
764                     ctrl_panel.set({
765                         "cmp-par-opt": opts,
766                         "cmp-par-dis": False,
767                         "cmp-par-val": c_sel["parameter"]
768                     })
769                     opts = list()
770                     for itm in ctrl_panel.get(f"{c_sel['parameter']}-opt"):
771                         set_val = ctrl_panel.get(f"{c_sel['parameter']}-val")
772                         if isinstance(set_val, list):
773                             if itm["value"] not in set_val:
774                                 opts.append(itm)
775                         else:
776                             if itm["value"] != set_val:
777                                 opts.append(itm)
778                     ctrl_panel.set({
779                         "cmp-val-opt": opts,
780                         "cmp-val-dis": False,
781                         "cmp-val-val": c_sel["value"]
782                     })
783                     on_draw = True
784             elif trigger.type == "normalize":
785                 ctrl_panel.set({"normalize-val": normalize})
786                 on_draw = True
787             elif trigger.type == "ctrl-dd":
788                 if trigger.idx == "dut":
789                     try:
790                         opts = generate_options(self._tbs[trigger.value].keys())
791                         disabled = False
792                     except KeyError:
793                         opts = list()
794                         disabled = True
795                     ctrl_panel.set({
796                         "dut-val": trigger.value,
797                         "dutver-opt": opts,
798                         "dutver-dis": disabled,
799                         "dutver-val": str(),
800                         "infra-opt": list(),
801                         "infra-dis": True,
802                         "infra-val": str(),
803                         "core-opt": list(),
804                         "core-val": list(),
805                         "frmsize-opt": list(),
806                         "frmsize-val": list(),
807                         "ttype-opt": list(),
808                         "ttype-val": list(),
809                         "cmp-par-opt": list(),
810                         "cmp-par-dis": True,
811                         "cmp-par-val": str(),
812                         "cmp-val-opt": list(),
813                         "cmp-val-dis": True,
814                         "cmp-val-val": str()
815                     })
816                 elif trigger.idx == "dutver":
817                     try:
818                         dut = ctrl_panel.get("dut-val")
819                         dver = self._tbs[dut][trigger.value]
820                         opts = generate_options(dver.keys())
821                         disabled = False
822                     except KeyError:
823                         opts = list()
824                         disabled = True
825                     ctrl_panel.set({
826                         "dutver-val": trigger.value,
827                         "infra-opt": opts,
828                         "infra-dis": disabled,
829                         "infra-val": str(),
830                         "core-opt": list(),
831                         "core-val": list(),
832                         "frmsize-opt": list(),
833                         "frmsize-val": list(),
834                         "ttype-opt": list(),
835                         "ttype-val": list(),
836                         "cmp-par-opt": list(),
837                         "cmp-par-dis": True,
838                         "cmp-par-val": str(),
839                         "cmp-val-opt": list(),
840                         "cmp-val-dis": True,
841                         "cmp-val-val": str()
842                     })
843                 elif trigger.idx == "infra":
844                     dut = ctrl_panel.get("dut-val")
845                     dver = ctrl_panel.get("dutver-val")
846                     if all((dut, dver, trigger.value, )):
847                         driver = self._tbs[dut][dver][trigger.value]
848                         ctrl_panel.set({
849                             "infra-val": trigger.value,
850                             "core-opt": generate_options(driver["core"]),
851                             "core-val": list(),
852                             "frmsize-opt": generate_options(driver["fsize"]),
853                             "frmsize-val": list(),
854                             "ttype-opt": generate_options(driver["ttype"]),
855                             "ttype-val": list(),
856                             "cmp-par-opt": list(),
857                             "cmp-par-dis": True,
858                             "cmp-par-val": str(),
859                             "cmp-val-opt": list(),
860                             "cmp-val-dis": True,
861                             "cmp-val-val": str()
862                         })
863                 elif trigger.idx == "cmpprm":
864                     value = trigger.value
865                     opts = list()
866                     for itm in ctrl_panel.get(f"{value}-opt"):
867                         set_val = ctrl_panel.get(f"{value}-val")
868                         if isinstance(set_val, list):
869                             if itm["value"] == "Latency":
870                                 continue
871                             if itm["value"] not in set_val:
872                                 opts.append(itm)
873                         else:
874                             if itm["value"] != set_val:
875                                 opts.append(itm)
876                     ctrl_panel.set({
877                         "cmp-par-val": value,
878                         "cmp-val-opt": opts,
879                         "cmp-val-dis": False,
880                         "cmp-val-val": str()
881                     })
882                 elif trigger.idx == "cmpval":
883                     ctrl_panel.set({"cmp-val-val": trigger.value})
884                     selected["reference"] = {
885                         "set": True,
886                         "selection": {
887                             "dut": ctrl_panel.get("dut-val"),
888                             "dutver": ctrl_panel.get("dutver-val"),
889                             "infra": ctrl_panel.get("infra-val"),
890                             "core": ctrl_panel.get("core-val"),
891                             "frmsize": ctrl_panel.get("frmsize-val"),
892                             "ttype": ctrl_panel.get("ttype-val")
893                         }
894                     }
895                     selected["compare"] = {
896                         "set": True,
897                         "parameter": ctrl_panel.get("cmp-par-val"),
898                         "value": trigger.value
899                     }
900                     on_draw = True
901             elif trigger.type == "ctrl-cl":
902                 ctrl_panel.set({f"{trigger.idx}-val": trigger.value})
903                 if all((ctrl_panel.get("core-val"),
904                         ctrl_panel.get("frmsize-val"),
905                         ctrl_panel.get("ttype-val"), )):
906                     if "Latency" in ctrl_panel.get("ttype-val"):
907                         ctrl_panel.set({"ttype-val": ["Latency", ]})
908                     opts = list()
909                     for itm, label in CMP_PARAMS.items():
910                         if "Latency" in ctrl_panel.get("ttype-val") and \
911                                 itm == "ttype":
912                             continue
913                         if len(ctrl_panel.get(f"{itm}-opt")) > 1:
914                             if isinstance(ctrl_panel.get(f"{itm}-val"), list):
915                                 if len(ctrl_panel.get(f"{itm}-opt")) == \
916                                         len(ctrl_panel.get(f"{itm}-val")):
917                                     continue
918                             opts.append({"label": label, "value": itm})
919                     ctrl_panel.set({
920                         "cmp-par-opt": opts,
921                         "cmp-par-dis": False,
922                         "cmp-par-val": str(),
923                         "cmp-val-opt": list(),
924                         "cmp-val-dis": True,
925                         "cmp-val-val": str()
926                     })
927                 else:
928                     ctrl_panel.set({
929                         "cmp-par-opt": list(),
930                         "cmp-par-dis": True,
931                         "cmp-par-val": str(),
932                         "cmp-val-opt": list(),
933                         "cmp-val-dis": True,
934                         "cmp-val-val": str()
935                     })
936             elif trigger.type == "table" and trigger.idx == "comparison":
937                 filtered_data = filter_table_data(
938                     store_table_data,
939                     table_filter[0]
940                 )
941                 table_data = [filtered_data, ]
942
943             if all((on_draw, selected["reference"]["set"],
944                     selected["compare"]["set"], )):
945                 title, table = comparison_table(self._data, selected, normalize)
946                 plotting_area = self._get_plotting_area(
947                     title=title,
948                     table=table,
949                     url=gen_new_url(
950                         parsed_url,
951                         params={"selected": selected, "norm": normalize}
952                     )
953                 )
954                 store_table_data = table.to_dict("records")
955                 filtered_data = store_table_data
956                 if table_data:
957                     table_data = [store_table_data, ]
958
959             ret_val = [
960                 ctrl_panel.panel,
961                 selected,
962                 store_table_data,
963                 filtered_data,
964                 plotting_area,
965                 table_data
966             ]
967             ret_val.extend(ctrl_panel.values)
968             return ret_val
969
970         @app.callback(
971             Output("plot-mod-url", "is_open"),
972             Input("plot-btn-url", "n_clicks"),
973             State("plot-mod-url", "is_open")
974         )
975         def toggle_plot_mod_url(n, is_open):
976             """Toggle the modal window with url.
977             """
978             if n:
979                 return not is_open
980             return is_open
981
982         @app.callback(
983             Output("download-iterative-data", "data"),
984             State("store-table-data", "data"),
985             State("store-filtered-table-data", "data"),
986             Input("plot-btn-download", "n_clicks"),
987             prevent_initial_call=True
988         )
989         def _download_comparison_data(
990                 table_data: list,
991                 filtered_table_data: list,
992                 _: int
993             ) -> dict:
994             """Download the data.
995
996             :param table_data: Original unfiltered table data.
997             :param filtered_table_data: Filtered table data.
998             :type table_data: list
999             :type filtered_table_data: list
1000             :returns: dict of data frame content (base64 encoded) and meta data
1001                 used by the Download component.
1002             :rtype: dict
1003             """
1004
1005             if not table_data:
1006                 raise PreventUpdate
1007
1008             if filtered_table_data:
1009                 table = pd.DataFrame.from_records(filtered_table_data)
1010             else:
1011                 table = pd.DataFrame.from_records(table_data)
1012
1013             return dcc.send_data_frame(table.to_csv, C.COMP_DOWNLOAD_FILE_NAME)
1014
1015         @app.callback(
1016             Output("offcanvas-documentation", "is_open"),
1017             Input("btn-documentation", "n_clicks"),
1018             State("offcanvas-documentation", "is_open")
1019         )
1020         def toggle_offcanvas_documentation(n_clicks, is_open):
1021             if n_clicks:
1022                 return not is_open
1023             return is_open