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