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