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