C-Dash: Add search in tests
[csit.git] / csit.infra.dash / app / cdash / report / layout.py
index 8391ed5..400fd60 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
+# Copyright (c) 2024 Cisco and/or its affiliates.
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 # You may obtain a copy of the License at:
@@ -14,6 +14,7 @@
 """Plotly Dash HTML layout override.
 """
 
+
 import logging
 import pandas as pd
 import dash_bootstrap_components as dbc
@@ -31,9 +32,9 @@ from ..utils.constants import Constants as C
 from ..utils.control_panel import ControlPanel
 from ..utils.trigger import Trigger
 from ..utils.utils import show_tooltip, label, sync_checklists, gen_new_url, \
-    generate_options, get_list_group_items
+    generate_options, get_list_group_items, navbar_report, \
+    show_iterative_graph_data
 from ..utils.url_processing import url_decode
-from ..data.data import Data
 from .graphs import graph_iterative, select_iterative_data
 
 
@@ -76,8 +77,14 @@ class Layout:
     """The layout of the dash app and the callbacks.
     """
 
-    def __init__(self, app: Flask, releases: list, html_layout_file: str,
-        graph_layout_file: str, data_spec_file: str, tooltip_file: str) -> None:
+    def __init__(
+            self,
+            app: Flask,
+            data_iterative: pd.DataFrame,
+            html_layout_file: str,
+            graph_layout_file: str,
+            tooltip_file: str
+        ) -> None:
         """Initialization:
         - save the input parameters,
         - read and pre-process the data,
@@ -86,51 +93,32 @@ class Layout:
         - read tooltips from the tooltip file.
 
         :param app: Flask application running the dash application.
-        :param releases: Lis of releases to be displayed.
         :param html_layout_file: Path and name of the file specifying the HTML
             layout of the dash application.
         :param graph_layout_file: Path and name of the file with layout of
             plot.ly graphs.
-        :param data_spec_file: Path and name of the file specifying the data to
-            be read from parquets for this application.
         :param tooltip_file: Path and name of the yaml file specifying the
             tooltips.
         :type app: Flask
-        :type releases: list
         :type html_layout_file: str
         :type graph_layout_file: str
-        :type data_spec_file: str
         :type tooltip_file: str
         """
 
         # Inputs
         self._app = app
-        self.releases = releases
         self._html_layout_file = html_layout_file
         self._graph_layout_file = graph_layout_file
-        self._data_spec_file = data_spec_file
         self._tooltip_file = tooltip_file
-
-        # Read the data:
-        self._data = pd.DataFrame()
-        for rls in releases:
-            data_mrr = Data(self._data_spec_file, True).\
-                read_iterative_mrr(release=rls)
-            data_mrr["release"] = rls
-            data_ndrpdr = Data(self._data_spec_file, True).\
-                read_iterative_ndrpdr(release=rls)
-            data_ndrpdr["release"] = rls
-            self._data = pd.concat(
-                [self._data, data_mrr, data_ndrpdr],
-                ignore_index=True
-            )
+        self._data = data_iterative
 
         # Get structure of tests:
         tbs = dict()
-        cols = ["job", "test_id", "test_type", "dut_version", "release"]
+        cols = [
+            "job", "test_id", "test_type", "dut_version", "tg_type", "release"
+        ]
         for _, row in self._data[cols].drop_duplicates().iterrows():
             rls = row["release"]
-            ttype = row["test_type"]
             lst_job = row["job"].split("-")
             dut = lst_job[1]
             d_ver = row["dut_version"]
@@ -139,7 +127,7 @@ class Layout:
             if dut == "dpdk":
                 area = "dpdk"
             else:
-                area = "-".join(lst_test_id[3:-2])
+                area = ".".join(lst_test_id[3:-2])
             suite = lst_test_id[-2].replace("2n1l-", "").replace("1n1l-", "").\
                 replace("2n-", "")
             test = lst_test_id[-1]
@@ -163,37 +151,34 @@ class Layout:
                 tbs[rls][dut] = dict()
             if tbs[rls][dut].get(d_ver, None) is None:
                 tbs[rls][dut][d_ver] = dict()
-            if tbs[rls][dut][d_ver].get(infra, None) is None:
-                tbs[rls][dut][d_ver][infra] = dict()
-            if tbs[rls][dut][d_ver][infra].get(area, None) is None:
-                tbs[rls][dut][d_ver][infra][area] = dict()
-            if tbs[rls][dut][d_ver][infra][area].get(test, None) is None:
-                tbs[rls][dut][d_ver][infra][area][test] = dict()
-                tbs[rls][dut][d_ver][infra][area][test]["core"] = list()
-                tbs[rls][dut][d_ver][infra][area][test]["frame-size"] = list()
-                tbs[rls][dut][d_ver][infra][area][test]["test-type"] = list()
-            if core.upper() not in \
-                    tbs[rls][dut][d_ver][infra][area][test]["core"]:
-                tbs[rls][dut][d_ver][infra][area][test]["core"].append(
-                    core.upper()
-                )
-            if framesize.upper() not in \
-                        tbs[rls][dut][d_ver][infra][area][test]["frame-size"]:
-                tbs[rls][dut][d_ver][infra][area][test]["frame-size"].append(
-                    framesize.upper()
-                )
-            if ttype == "mrr":
-                if "MRR" not in \
-                        tbs[rls][dut][d_ver][infra][area][test]["test-type"]:
-                    tbs[rls][dut][d_ver][infra][area][test]["test-type"].append(
-                        "MRR"
-                    )
-            elif ttype == "ndrpdr":
-                if "NDR" not in \
-                        tbs[rls][dut][d_ver][infra][area][test]["test-type"]:
-                    tbs[rls][dut][d_ver][infra][area][test]["test-type"].extend(
-                        ("NDR", "PDR", )
-                    )
+            if tbs[rls][dut][d_ver].get(area, None) is None:
+                tbs[rls][dut][d_ver][area] = dict()
+            if tbs[rls][dut][d_ver][area].get(test, None) is None:
+                tbs[rls][dut][d_ver][area][test] = dict()
+            if tbs[rls][dut][d_ver][area][test].get(infra, None) is None:
+                tbs[rls][dut][d_ver][area][test][infra] = {
+                    "core": list(),
+                    "frame-size": list(),
+                    "test-type": list()
+                }
+            tst_params = tbs[rls][dut][d_ver][area][test][infra]
+            if core.upper() not in tst_params["core"]:
+                tst_params["core"].append(core.upper())
+            if framesize.upper() not in tst_params["frame-size"]:
+                tst_params["frame-size"].append(framesize.upper())
+            if row["test_type"] == "mrr":
+                if "MRR" not in tst_params["test-type"]:
+                    tst_params["test-type"].append("MRR")
+            elif row["test_type"] == "ndrpdr":
+                if "NDR" not in tst_params["test-type"]:
+                    tst_params["test-type"].extend(("NDR", "PDR", ))
+            elif row["test_type"] == "hoststack" and \
+                    row["tg_type"] in ("iperf", "vpp"):
+                if "BPS" not in tst_params["test-type"]:
+                    tst_params["test-type"].append("BPS")
+            elif row["test_type"] == "hoststack" and row["tg_type"] == "ab":
+                if "CPS" not in tst_params["test-type"]:
+                    tst_params["test-type"].extend(("CPS", "RPS"))
         self._spec_tbs = tbs
 
         # Read from files:
@@ -266,9 +251,7 @@ class Layout:
                     dbc.Row(
                         id="row-navbar",
                         class_name="g-0",
-                        children=[
-                            self._add_navbar()
-                        ]
+                        children=[navbar_report((True, False, False, False)), ]
                     ),
                     dbc.Row(
                         id="row-main",
@@ -280,6 +263,32 @@ class Layout:
                             self._add_ctrl_col(),
                             self._add_plotting_col()
                         ]
+                    ),
+                    dbc.Spinner(
+                        dbc.Offcanvas(
+                            class_name="w-50",
+                            id="offcanvas-metadata",
+                            title="Throughput And Latency",
+                            placement="end",
+                            is_open=False,
+                            children=[
+                                dbc.Row(id="metadata-tput-lat"),
+                                dbc.Row(id="metadata-hdrh-graph")
+                            ]
+                        ),
+                        delay_show=C.SPINNER_DELAY
+                    ),
+                    dbc.Offcanvas(
+                        class_name="w-75",
+                        id="offcanvas-documentation",
+                        title="Documentation",
+                        placement="end",
+                        is_open=False,
+                        children=html.Iframe(
+                            src=C.URL_DOC_REL_NOTES,
+                            width="100%",
+                            height="100%"
+                        )
                     )
                 ]
             )
@@ -296,31 +305,6 @@ class Layout:
                 ]
             )
 
-    def _add_navbar(self):
-        """Add nav element with navigation panel. It is placed on the top.
-
-        :returns: Navigation bar.
-        :rtype: dbc.NavbarSimple
-        """
-        return dbc.NavbarSimple(
-            id="navbarsimple-main",
-            children=[
-                dbc.NavItem(
-                    dbc.NavLink(
-                        C.REPORT_TITLE,
-                        disabled=True,
-                        external_link=True,
-                        href="#"
-                    )
-                )
-            ],
-            brand=C.BRAND,
-            brand_href="/",
-            brand_external_link=True,
-            class_name="p-2",
-            fluid=True
-        )
-
     def _add_ctrl_col(self) -> dbc.Col:
         """Add column with controls. It is placed on the left side.
 
@@ -343,7 +327,7 @@ class Layout:
         return dbc.Col(
             id="col-plotting-area",
             children=[
-                dcc.Loading(
+                dbc.Spinner(
                     children=[
                         dbc.Row(
                             id="plotting-area",
@@ -444,14 +428,13 @@ class Layout:
                             dbc.InputGroupText(
                                 children=show_tooltip(
                                     self._tooltips,
-                                    "help-infra",
-                                    "Infra"
+                                    "help-area",
+                                    "Area"
                                 )
                             ),
                             dbc.Select(
-                                id={"type": "ctrl-dd", "index": "phy"},
-                                placeholder=\
-                                    "Select a Physical Test Bed Topology..."
+                                id={"type": "ctrl-dd", "index": "area"},
+                                placeholder="Select an Area..."
                             )
                         ],
                         size="sm"
@@ -466,13 +449,13 @@ class Layout:
                             dbc.InputGroupText(
                                 children=show_tooltip(
                                     self._tooltips,
-                                    "help-area",
-                                    "Area"
+                                    "help-test",
+                                    "Test"
                                 )
                             ),
                             dbc.Select(
-                                id={"type": "ctrl-dd", "index": "area"},
-                                placeholder="Select an Area..."
+                                id={"type": "ctrl-dd", "index": "test"},
+                                placeholder="Select a Test..."
                             )
                         ],
                         size="sm"
@@ -487,13 +470,14 @@ class Layout:
                             dbc.InputGroupText(
                                 children=show_tooltip(
                                     self._tooltips,
-                                    "help-test",
-                                    "Test"
+                                    "help-infra",
+                                    "Infra"
                                 )
                             ),
                             dbc.Select(
-                                id={"type": "ctrl-dd", "index": "test"},
-                                placeholder="Select a Test..."
+                                id={"type": "ctrl-dd", "index": "phy"},
+                                placeholder=\
+                                    "Select a Physical Test Bed Topology..."
                             )
                         ],
                         size="sm"
@@ -683,34 +667,49 @@ class Layout:
                         class_name="overflow-auto p-0",
                         id="lg-selected",
                         children=[],
-                        style={"max-height": "14em"},
+                        style={"max-height": "20em"},
                         flush=True
                     )
                 ]
             ),
-            dbc.Row(
+            dbc.Stack(
                 id="row-btns-sel-tests",
                 class_name="g-0 p-1",
                 style=C.STYLE_DISABLED,
+                gap=2,
                 children=[
-                    dbc.ButtonGroup(
-                        children=[
-                            dbc.Button(
-                                id={"type": "ctrl-btn", "index": "rm-test"},
-                                children="Remove Selected",
-                                class_name="w-100",
-                                color="info",
-                                disabled=False
-                            ),
-                            dbc.Button(
-                                id={"type": "ctrl-btn", "index": "rm-test-all"},
-                                children="Remove All",
-                                class_name="w-100",
-                                color="info",
-                                disabled=False
-                            )
-                        ]
-                    )
+                    dbc.ButtonGroup(children=[
+                        dbc.Button(
+                            id={"type": "ctrl-btn", "index": "rm-test"},
+                            children="Remove Selected",
+                            class_name="w-100",
+                            color="info",
+                            disabled=False
+                        ),
+                        dbc.Button(
+                            id={"type": "ctrl-btn", "index": "rm-test-all"},
+                            children="Remove All",
+                            class_name="w-100",
+                            color="info",
+                            disabled=False
+                        )
+                    ]),
+                    dbc.ButtonGroup(children=[
+                        dbc.Button(
+                            id="plot-btn-url",
+                            children="Show URL",
+                            class_name="w-100",
+                            color="info",
+                            disabled=False
+                        ),
+                        dbc.Button(
+                            id="plot-btn-download",
+                            children="Download Data",
+                            class_name="w-100",
+                            color="info",
+                            disabled=False
+                        )
+                    ])
                 ]
             )
         ]
@@ -736,81 +735,67 @@ class Layout:
         if not tests:
             return C.PLACEHOLDER
 
-        figs = graph_iterative(self._data, tests, self._graph_layout, normalize)
+        graphs = \
+            graph_iterative(self._data, tests, self._graph_layout, normalize)
 
-        if not figs[0]:
+        if not graphs[0]:
             return C.PLACEHOLDER
-
-        row_items = [
-            dbc.Col(
+        
+        tab_items = [
+            dbc.Tab(
                 children=dcc.Graph(
                     id={"type": "graph", "index": "tput"},
-                    figure=figs[0]
+                    figure=graphs[0]
                 ),
-                class_name="g-0 p-1",
-                width=6
+                label="Throughput",
+                tab_id="tab-tput"
             )
         ]
 
-        if figs[1]:
-            row_items.append(
-                dbc.Col(
+        if graphs[1]:
+            tab_items.append(
+                dbc.Tab(
+                    children=dcc.Graph(
+                        id={"type": "graph", "index": "bandwidth"},
+                        figure=graphs[1]
+                    ),
+                    label="Bandwidth",
+                    tab_id="tab-bandwidth"
+                )
+            )
+
+        if graphs[2]:
+            tab_items.append(
+                dbc.Tab(
                     children=dcc.Graph(
                         id={"type": "graph", "index": "lat"},
-                        figure=figs[1]
+                        figure=graphs[2]
                     ),
-                    class_name="g-0 p-1",
-                    width=6
+                    label="Latency",
+                    tab_id="tab-lat"
                 )
             )
 
         return [
             dbc.Row(
-                children=row_items,
-                class_name="g-0 p-0",
+                dbc.Tabs(
+                    children=tab_items,
+                    id="tabs",
+                    active_tab="tab-tput",
+                ),
+                class_name="g-0 p-0"
             ),
-            dbc.Row(
+            dbc.Modal(
                 [
-                    dbc.Col([html.Div(
-                        [
-                            dbc.Button(
-                                id="plot-btn-url",
-                                children="Show URL",
-                                class_name="me-1",
-                                color="info",
-                                style={
-                                    "text-transform": "none",
-                                    "padding": "0rem 1rem"
-                                }
-                            ),
-                            dbc.Modal(
-                                [
-                                    dbc.ModalHeader(dbc.ModalTitle("URL")),
-                                    dbc.ModalBody(url)
-                                ],
-                                id="plot-mod-url",
-                                size="xl",
-                                is_open=False,
-                                scrollable=True
-                            ),
-                            dbc.Button(
-                                id="plot-btn-download",
-                                children="Download Data",
-                                class_name="me-1",
-                                color="info",
-                                style={
-                                    "text-transform": "none",
-                                    "padding": "0rem 1rem"
-                                }
-                            ),
-                            dcc.Download(id="download-iterative-data")
-                        ],
-                        className=\
-                            "d-grid gap-0 d-md-flex justify-content-md-end"
-                    )])
+                    dbc.ModalHeader(dbc.ModalTitle("URL")),
+                    dbc.ModalBody(url)
                 ],
-                class_name="g-0 p-0"
-            )
+                id="plot-mod-url",
+                size="xl",
+                is_open=False,
+                scrollable=True
+            ),
+            dcc.Download(id="download-iterative-data")
         ]
 
     def callbacks(self, app):
@@ -906,15 +891,15 @@ class Layout:
                 try:
                     store_sel = literal_eval(url_params["store_sel"][0])
                     normalize = literal_eval(url_params["norm"][0])
-                except (KeyError, IndexError):
+                except (KeyError, IndexError, AttributeError):
                     pass
                 if store_sel:
                     row_card_sel_tests = C.STYLE_ENABLED
                     row_btns_sel_tests = C.STYLE_ENABLED
                     last_test = store_sel[-1]
                     test = self._spec_tbs[last_test["rls"]][last_test["dut"]]\
-                        [last_test["dutver"]][last_test["phy"]]\
-                            [last_test["area"]][last_test["test"]]
+                        [last_test["dutver"]][last_test["area"]]\
+                            [last_test["test"]][last_test["phy"]]
                     ctrl_panel.set({
                         "dd-rls-val": last_test["rls"],
                         "dd-dut-val": last_test["dut"],
@@ -928,27 +913,27 @@ class Layout:
                                 [last_test["dut"]].keys()
                         ),
                         "dd-dutver-dis": False,
-                        "dd-phy-val": last_test["phy"],
-                        "dd-phy-opt": generate_options(
-                            self._spec_tbs[last_test["rls"]][last_test["dut"]]\
-                                [last_test["dutver"]].keys()
-                        ),
-                        "dd-phy-dis": False,
                         "dd-area-val": last_test["area"],
                         "dd-area-opt": [
                             {"label": label(v), "value": v} for v in \
                                 sorted(self._spec_tbs[last_test["rls"]]\
-                                    [last_test["dut"]][last_test["dutver"]]\
-                                        [last_test["phy"]].keys())
+                                    [last_test["dut"]]\
+                                        [last_test["dutver"]].keys())
                         ],
                         "dd-area-dis": False,
                         "dd-test-val": last_test["test"],
                         "dd-test-opt": generate_options(
                             self._spec_tbs[last_test["rls"]][last_test["dut"]]\
-                                [last_test["dutver"]][last_test["phy"]]\
-                                    [last_test["area"]].keys()
+                                [last_test["dutver"]][last_test["area"]].keys()
                         ),
                         "dd-test-dis": False,
+                        "dd-phy-val": last_test["phy"],
+                        "dd-phy-opt": generate_options(
+                            self._spec_tbs[last_test["rls"]][last_test["dut"]]\
+                                [last_test["dutver"]][last_test["area"]]\
+                                    [last_test["test"]].keys()
+                        ),
+                        "dd-phy-dis": False,
                         "cl-core-opt": generate_options(test["core"]),
                         "cl-core-val": [last_test["core"].upper(), ],
                         "cl-core-all-val": list(),
@@ -1051,22 +1036,23 @@ class Layout:
                         rls = ctrl_panel.get("dd-rls-val")
                         dut = ctrl_panel.get("dd-dut-val")
                         dutver = self._spec_tbs[rls][dut][trigger.value]
-                        options = generate_options(dutver.keys())
+                        options = [{"label": label(v), "value": v} \
+                            for v in sorted(dutver.keys())]
                         disabled = False
                     except KeyError:
                         options = list()
                         disabled = True
                     ctrl_panel.set({
                         "dd-dutver-val": trigger.value,
-                        "dd-phy-val": str(),
-                        "dd-phy-opt": options,
-                        "dd-phy-dis": disabled,
                         "dd-area-val": str(),
-                        "dd-area-opt": list(),
-                        "dd-area-dis": True,
+                        "dd-area-opt": options,
+                        "dd-area-dis": disabled,
                         "dd-test-val": str(),
                         "dd-test-opt": list(),
                         "dd-test-dis": True,
+                        "dd-phy-val": str(),
+                        "dd-phy-opt": list(),
+                        "dd-phy-dis": True,
                         "cl-core-opt": list(),
                         "cl-core-val": list(),
                         "cl-core-all-val": list(),
@@ -1081,26 +1067,25 @@ class Layout:
                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
                         "btn-add-dis": True
                     })
-                elif trigger.idx == "phy":
+                elif trigger.idx == "area":
                     try:
                         rls = ctrl_panel.get("dd-rls-val")
                         dut = ctrl_panel.get("dd-dut-val")
                         dutver = ctrl_panel.get("dd-dutver-val")
-                        phy = self._spec_tbs[rls][dut][dutver][trigger.value]
-                        options = [{"label": label(v), "value": v} \
-                            for v in sorted(phy.keys())]
+                        area = self._spec_tbs[rls][dut][dutver][trigger.value]
+                        options = generate_options(area.keys())
                         disabled = False
                     except KeyError:
                         options = list()
                         disabled = True
                     ctrl_panel.set({
-                        "dd-phy-val": trigger.value,
-                        "dd-area-val": str(),
-                        "dd-area-opt": options,
-                        "dd-area-dis": disabled,
+                        "dd-area-val": trigger.value,
                         "dd-test-val": str(),
-                        "dd-test-opt": list(),
-                        "dd-test-dis": True,
+                        "dd-test-opt": options,
+                        "dd-test-dis": disabled,
+                        "dd-phy-val": str(),
+                        "dd-phy-opt": list(),
+                        "dd-phy-dis": True,
                         "cl-core-opt": list(),
                         "cl-core-val": list(),
                         "cl-core-all-val": list(),
@@ -1115,24 +1100,24 @@ class Layout:
                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
                         "btn-add-dis": True
                     })
-                elif trigger.idx == "area":
+                elif trigger.idx == "test":
                     try:
                         rls = ctrl_panel.get("dd-rls-val")
                         dut = ctrl_panel.get("dd-dut-val")
                         dutver = ctrl_panel.get("dd-dutver-val")
-                        phy = ctrl_panel.get("dd-phy-val")
-                        area = \
-                            self._spec_tbs[rls][dut][dutver][phy][trigger.value]
-                        options = generate_options(area.keys())
+                        area = ctrl_panel.get("dd-area-val")
+                        test = self._spec_tbs[rls][dut][dutver][area]\
+                            [trigger.value]
+                        options = generate_options(test.keys())
                         disabled = False
                     except KeyError:
                         options = list()
                         disabled = True
                     ctrl_panel.set({
-                        "dd-area-val": trigger.value,
-                        "dd-test-val": str(),
-                        "dd-test-opt": options,
-                        "dd-test-dis": disabled,
+                        "dd-test-val": trigger.value,
+                        "dd-phy-val": str(),
+                        "dd-phy-opt": options,
+                        "dd-phy-dis": disabled,
                         "cl-core-opt": list(),
                         "cl-core-val": list(),
                         "cl-core-all-val": list(),
@@ -1147,28 +1132,28 @@ class Layout:
                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
                         "btn-add-dis": True
                     })
-                elif trigger.idx == "test":
+                elif trigger.idx == "phy":
                     rls = ctrl_panel.get("dd-rls-val")
                     dut = ctrl_panel.get("dd-dut-val")
                     dutver = ctrl_panel.get("dd-dutver-val")
-                    phy = ctrl_panel.get("dd-phy-val")
                     area = ctrl_panel.get("dd-area-val")
-                    if all((rls, dut, dutver, phy, area, trigger.value, )):
-                        test = self._spec_tbs[rls][dut][dutver][phy][area]\
+                    test = ctrl_panel.get("dd-test-val")
+                    if all((rls, dut, dutver, area, test, trigger.value, )):
+                        phy = self._spec_tbs[rls][dut][dutver][area][test]\
                             [trigger.value]
                         ctrl_panel.set({
-                            "dd-test-val": trigger.value,
-                            "cl-core-opt": generate_options(test["core"]),
+                            "dd-phy-val": trigger.value,
+                            "cl-core-opt": generate_options(phy["core"]),
                             "cl-core-val": list(),
                             "cl-core-all-val": list(),
                             "cl-core-all-opt": C.CL_ALL_ENABLED,
                             "cl-frmsize-opt": \
-                                generate_options(test["frame-size"]),
+                                generate_options(phy["frame-size"]),
                             "cl-frmsize-val": list(),
                             "cl-frmsize-all-val": list(),
                             "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
                             "cl-tsttype-opt": \
-                                generate_options(test["test-type"]),
+                                generate_options(phy["test-type"]),
                             "cl-tsttype-val": list(),
                             "cl-tsttype-all-val": list(),
                             "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
@@ -1251,7 +1236,9 @@ class Layout:
 
             if on_draw:
                 if store_sel:
-                    lg_selected = get_list_group_items(store_sel)
+                    lg_selected = get_list_group_items(
+                        store_sel, "sel-cl", add_index=True
+                    )
                     plotting_area = self._get_plotting_area(
                         store_sel,
                         bool(normalize),
@@ -1281,15 +1268,16 @@ class Layout:
 
         @app.callback(
             Output("plot-mod-url", "is_open"),
-            [Input("plot-btn-url", "n_clicks")],
-            [State("plot-mod-url", "is_open")],
+            Output("plot-btn-url", "n_clicks"),
+            Input("plot-btn-url", "n_clicks"),
+            State("plot-mod-url", "is_open")
         )
         def toggle_plot_mod_url(n, is_open):
             """Toggle the modal window with url.
             """
             if n:
-                return not is_open
-            return is_open
+                return not is_open, 0
+            return is_open, 0
 
         @app.callback(
             Output("download-iterative-data", "data"),
@@ -1297,7 +1285,7 @@ class Layout:
             Input("plot-btn-download", "n_clicks"),
             prevent_initial_call=True
         )
-        def _download_trending_data(store_sel, _):
+        def _download_iterative_data(store_sel, _):
             """Download the data
 
             :param store_sel: List of tests selected by user stored in the
@@ -1319,3 +1307,38 @@ class Layout:
                 df = pd.concat([df, sel_data], ignore_index=True)
 
             return dcc.send_data_frame(df.to_csv, C.REPORT_DOWNLOAD_FILE_NAME)
+
+        @app.callback(
+            Output("metadata-tput-lat", "children"),
+            Output("metadata-hdrh-graph", "children"),
+            Output("offcanvas-metadata", "is_open"),
+            Input({"type": "graph", "index": ALL}, "clickData"),
+            prevent_initial_call=True
+        )
+        def _show_metadata_from_graphs(graph_data: dict) -> tuple:
+            """Generates the data for the offcanvas displayed when a particular
+            point in a graph is clicked on.
+
+            :param graph_data: The data from the clicked point in the graph.
+            :type graph_data: dict
+            :returns: The data to be displayed on the offcanvas and the
+                information to show the offcanvas.
+            :rtype: tuple(list, list, bool)
+            """
+
+            trigger = Trigger(callback_context.triggered)
+            if not trigger.value:
+                raise PreventUpdate
+
+            return show_iterative_graph_data(
+                    trigger, graph_data, self._graph_layout)
+
+        @app.callback(
+            Output("offcanvas-documentation", "is_open"),
+            Input("btn-documentation", "n_clicks"),
+            State("offcanvas-documentation", "is_open")
+        )
+        def toggle_offcanvas_documentation(n_clicks, is_open):
+            if n_clicks:
+                return not is_open
+            return is_open