feat(uti): Cover theme sync
[csit.git] / resources / tools / dash / app / pal / trending / layout.py
index 6be71ac..422488d 100644 (file)
 """Plotly Dash HTML layout override.
 """
 
 """Plotly Dash HTML layout override.
 """
 
-
-import json
 import pandas as pd
 
 from dash import dcc
 from dash import html
 from dash import callback_context, no_update
 import pandas as pd
 
 from dash import dcc
 from dash import html
 from dash import callback_context, no_update
-from dash import Input, Output, State, callback
+from dash import Input, Output, State
 from dash.exceptions import PreventUpdate
 from dash.exceptions import PreventUpdate
+import dash_bootstrap_components as dbc
 from yaml import load, FullLoader, YAMLError
 from datetime import datetime, timedelta
 
 from ..data.data import Data
 from yaml import load, FullLoader, YAMLError
 from datetime import datetime, timedelta
 
 from ..data.data import Data
-from .graphs import trending_tput
+from .graphs import graph_trending, graph_hdrh_latency, \
+    select_trending_data
 
 
 class Layout:
     """
     """
 
 
 
 class Layout:
     """
     """
 
+    NO_GRAPH = {"data": [], "layout": {}, "frames": []}
+
     def __init__(self, app, html_layout_file, spec_file, graph_layout_file,
         data_spec_file):
         """
     def __init__(self, app, html_layout_file, spec_file, graph_layout_file,
         data_spec_file):
         """
@@ -50,12 +52,12 @@ class Layout:
         data_mrr = Data(
             data_spec_file=self._data_spec_file,
             debug=True
         data_mrr = Data(
             data_spec_file=self._data_spec_file,
             debug=True
-        ).read_trending_mrr()
+        ).read_trending_mrr(days=5)
 
         data_ndrpdr = Data(
             data_spec_file=self._data_spec_file,
             debug=True
 
         data_ndrpdr = Data(
             data_spec_file=self._data_spec_file,
             debug=True
-        ).read_trending_ndrpdr()
+        ).read_trending_ndrpdr(days=14)
 
         self._data = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
 
 
         self._data = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
 
@@ -128,198 +130,297 @@ class Layout:
             return html.Div(
                 id="div-main",
                 children=[
             return html.Div(
                 id="div-main",
                 children=[
-                    dcc.Store(id="selected-tests"),
-                    self._add_ctrl_div(),
-                    self._add_plotting_div()
+                    dbc.Row(
+                        id="row-navbar",
+                        class_name="g-0",
+                        children=[
+                            self._add_navbar(),
+                        ]
+                    ),
+                    dcc.Loading(
+                        dbc.Offcanvas(
+                            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"),
+                            ]
+                        )
+                    ),
+                    dbc.Row(
+                        id="row-main",
+                        class_name="g-0 p-2",
+                        children=[
+                            dcc.Store(
+                                id="selected-tests"
+                            ),
+                            self._add_ctrl_col(),
+                            self._add_plotting_col(),
+                        ]
+                    )
                 ]
             )
         else:
             return html.Div(
                 ]
             )
         else:
             return html.Div(
-            id="div-main-error",
-            children="An Error Occured."
-        )
+                id="div-main-error",
+                children=[
+                    dbc.Alert(
+                        [
+                            "An Error Occured",
+                        ],
+                        color="danger",
+                    ),
+                ]
+            )
 
 
-    def _add_ctrl_div(self):
-        """Add div with controls. It is placed on the left side.
+    def _add_navbar(self):
+        """Add nav element with navigation panel. It is placed on the top.
         """
         """
-        return html.Div(
-            id="div-controls",
+        return dbc.NavbarSimple(
+            id="navbarsimple-main",
             children=[
             children=[
-                html.Div(
-                    id="div-controls-tabs",
-                    children=[
-                        self._add_ctrl_select(),
-                        self._add_ctrl_shown()
-                    ]
+                dbc.NavItem(
+                    dbc.NavLink(
+                        "Continuous Performance Trending",
+                        disabled=True,
+                        external_link=True,
+                        href="#"
+                    )
                 )
             ],
                 )
             ],
-            style={
-                "display": "inline-block",
-                "width": "18%",
-                "padding": "5px"
-            }
+            brand="Dashboard",
+            brand_href="/",
+            brand_external_link=True,
+            class_name="p-2",
+            fluid=True,
         )
 
         )
 
-    def _add_plotting_div(self):
-        """Add div with plots and tables. It is placed on the right side.
+    def _add_ctrl_col(self) -> dbc.Col:
+        """Add column with controls. It is placed on the left side.
         """
         """
-        return html.Div(
-            id="div-plotting-area",
+        return dbc.Col(
+            id="col-controls",
             children=[
             children=[
-                dcc.Loading(
-                    id="loading-graph",
-                    children=[
-                        dcc.Graph(
-                            id="graph"
-                        )
-                    ],
-                    type="circle"
-                ),
-                html.Div(
-                    children=[
-                        dcc.Markdown("""
-                        **Metadata**
-
-                        Click on data points in the graph.
-                        """),
-                        html.Pre(
-                            id="hover-metadata"
-                        )
-                    ]
-                )
+                self._add_ctrl_panel(),
+                self._add_ctrl_shown()
             ],
             ],
-            style={
-                "vertical-align": "top",
-                "display": "none",
-                "width": "80%",
-                "padding": "5px"
-            }
         )
 
         )
 
-    def _add_ctrl_shown(self):
+    def _add_plotting_col(self) -> dbc.Col:
+        """Add column with plots and tables. It is placed on the right side.
         """
         """
-        """
-        return html.Div(
-            id="div-ctrl-shown",
+        return dbc.Col(
+            id="col-plotting-area",
             children=[
             children=[
-                html.H5("Selected tests"),
-                html.Div(
-                    id="container-selected-tests",
+                dbc.Row(  # Throughput
+                    id="row-graph-tput",
+                    class_name="g-0 p-2",
                     children=[
                     children=[
-                        dcc.Checklist(
-                            id="cl-selected",
-                            options=[],
-                            labelStyle={"display": "block"}
-                        ),
-                        html.Button(
-                            id="btn-sel-remove",
-                            children="Remove Selected",
-                            disabled=False
-                        ),
-                        html.Button(
-                            id="btn-sel-display",
-                            children="Display",
-                            disabled=False
+                        dcc.Loading(
+                            dcc.Graph(id="graph-tput")
                         )
                     ]
                 ),
                         )
                     ]
                 ),
-            ]
+                dbc.Row(  # Latency
+                    id="row-graph-lat",
+                    class_name="g-0 p-2",
+                    children=[
+                        dcc.Loading(
+                            dcc.Graph(id="graph-latency")
+                        )
+                    ]
+                ),
+                dbc.Row(  # Download
+                    id="div-download",
+                    class_name="g-0",
+                    children=[
+                        dcc.Loading(children=[
+                            dbc.Button(
+                                id="btn-download-data",
+                                children=["Download Data"]
+                            ),
+                            dcc.Download(id="download-data")
+                        ])
+                    ]
+                )
+            ],
+            width=9,
         )
 
         )
 
-    def _add_ctrl_select(self):
+    def _add_ctrl_panel(self) -> dbc.Row:
         """
         """
         """
         """
-        return html.Div(
-            id="div-ctrl-select",
+        return dbc.Row(
+            id="row-ctrl-panel",
+            class_name="g-0",
             children=[
             children=[
-                html.H5("Physical Test Bed Topology, NIC and Driver"),
-                dcc.Dropdown(
+                dbc.Label("Physical Test Bed Topology, NIC and Driver"),
+                dbc.Select(
                     id="dd-ctrl-phy",
                     id="dd-ctrl-phy",
+                    className="p-2",
                     placeholder="Select a Physical Test Bed Topology...",
                     placeholder="Select a Physical Test Bed Topology...",
-                    multi=False,
-                    clearable=False,
                     options=[
                         {"label": k, "value": k} for k in self.spec_tbs.keys()
                     ],
                     options=[
                         {"label": k, "value": k} for k in self.spec_tbs.keys()
                     ],
+                    size="sm",
                 ),
                 ),
-                html.H5("Area"),
-                dcc.Dropdown(
+                dbc.Label("Area"),
+                dbc.Select(
                     id="dd-ctrl-area",
                     id="dd-ctrl-area",
+                    className="p-2",
                     placeholder="Select an Area...",
                     disabled=True,
                     placeholder="Select an Area...",
                     disabled=True,
-                    multi=False,
-                    clearable=False,
+                    size="sm",
                 ),
                 ),
-                html.H5("Test"),
-                dcc.Dropdown(
+                dbc.Label("Test"),
+                dbc.Select(
                     id="dd-ctrl-test",
                     id="dd-ctrl-test",
+                    className="p-2",
                     placeholder="Select a Test...",
                     disabled=True,
                     placeholder="Select a Test...",
                     disabled=True,
-                    multi=False,
-                    clearable=False,
+                    size="sm",
                 ),
                 ),
-                html.Div(
-                    id="div-ctrl-core",
+                dbc.Row(
+                    id="row-ctrl-core",
+                    class_name="g-0",
                     children=[
                     children=[
-                        html.H5("Number of Cores"),
-                        dcc.Checklist(
-                            id="cl-ctrl-core-all",
-                            options=[{"label": "All", "value": "all"}, ],
-                            labelStyle={"display": "inline-block"}
-                        ),
-                        dcc.Checklist(
-                            id="cl-ctrl-core",
-                            labelStyle={"display": "inline-block"}
-                        )
-                    ],
-                    style={"display": "none"}
+                        dbc.Label("Number of Cores"),
+                        dbc.Col([
+                            dbc.Checklist(
+                                id="cl-ctrl-core-all",
+                                options=[{"label": "All", "value": "all"}, ],
+                                inline=False,
+                                switch=False
+                            ),
+                        ], width=3),
+                        dbc.Col([
+                            dbc.Checklist(
+                                id="cl-ctrl-core",
+                                inline=True,
+                                switch=False
+                            )
+                        ])
+                    ]
                 ),
                 ),
-                html.Div(
-                    id="div-ctrl-framesize",
+                dbc.Row(
+                    id="row-ctrl-framesize",
+                    class_name="g-0",
                     children=[
                     children=[
-                        html.H5("Frame Size"),
-                        dcc.Checklist(
-                            id="cl-ctrl-framesize-all",
-                            options=[{"label": "All", "value": "all"}, ],
-                            labelStyle={"display": "inline-block"}
-                        ),
-                        dcc.Checklist(
-                            id="cl-ctrl-framesize",
-                            labelStyle={"display": "inline-block"}
-                        )
-                    ],
-                    style={"display": "none"}
+                        dbc.Label("Frame Size"),
+                        dbc.Col([
+                            dbc.Checklist(
+                                id="cl-ctrl-framesize-all",
+                                options=[{"label": "All", "value": "all"}, ],
+                                inline=True,
+                                switch=False
+                            ),
+                        ], width=3),
+                        dbc.Col([
+                            dbc.Checklist(
+                                id="cl-ctrl-framesize",
+                                inline=True,
+                                switch=False
+                            )
+                        ])
+                    ]
                 ),
                 ),
-                html.Div(
-                    id="div-ctrl-testtype",
+                dbc.Row(
+                    id="row-ctrl-testtype",
+                    class_name="g-0",
                     children=[
                     children=[
-                        html.H5("Test Type"),
-                        dcc.Checklist(
-                            id="cl-ctrl-testtype-all",
-                            options=[{"label": "All", "value": "all"}, ],
-                            labelStyle={"display": "inline-block"}
-                        ),
-                        dcc.Checklist(
-                            id="cl-ctrl-testtype",
-                            labelStyle={"display": "inline-block"}
+                        dbc.Label("Test Type"),
+                        dbc.Col([
+                            dbc.Checklist(
+                                id="cl-ctrl-testtype-all",
+                                options=[{"label": "All", "value": "all"}, ],
+                                inline=True,
+                                switch=False
+                            ),
+                        ], width=3),
+                        dbc.Col([
+                            dbc.Checklist(
+                                id="cl-ctrl-testtype",
+                                inline=True,
+                                switch=False
+                            )
+                        ])
+                    ]
+                ),
+                dbc.Row(
+                    class_name="g-0",
+                    children=[
+                        dbc.Button(
+                            id="btn-ctrl-add",
+                            children="Add",
                         )
                         )
-                    ],
-                    style={"display": "none"}
+                    ]
                 ),
                 ),
-                html.Button(
-                    id="btn-ctrl-add",
-                    children="Add",
-                    disabled=True
+                dbc.Row(
+                    class_name="g-0",
+                    children=[
+                        dcc.DatePickerRange(
+                            id="dpr-period",
+                            min_date_allowed=\
+                                datetime.utcnow()-timedelta(days=180),
+                            max_date_allowed=datetime.utcnow(),
+                            initial_visible_month=datetime.utcnow(),
+                            start_date=datetime.utcnow() - timedelta(days=180),
+                            end_date=datetime.utcnow(),
+                            display_format="D MMMM YY"
+                        )
+                    ]
+                )
+            ]
+        )
+
+    def _add_ctrl_shown(self) -> dbc.Row:
+        """
+        """
+        return dbc.Row(
+            id="div-ctrl-shown",
+            class_name="g-0",
+            children=[
+                dbc.Row(
+                    class_name="g-0",
+                    children=[
+                        dbc.Label("Selected tests"),
+                        dbc.Checklist(
+                            id="cl-selected",
+                            options=[],
+                            inline=False
+                        )
+                    ]
                 ),
                 ),
-                html.Br(),
-                dcc.DatePickerRange(
-                    id="dpr-period",
-                    min_date_allowed=datetime.utcnow() - timedelta(days=180),
-                    max_date_allowed=datetime.utcnow(),
-                    initial_visible_month=datetime.utcnow(),
-                    start_date=datetime.utcnow() - timedelta(days=180),
-                    end_date=datetime.utcnow(),
-                    display_format="D MMMM YY"
+                dbc.Row(
+                    class_name="g-0",
+                    children=[
+                        dbc.ButtonGroup(
+                            [
+                                dbc.Button(
+                                    id="btn-sel-remove-all",
+                                    children="Remove All",
+                                    color="secondary",
+                                    disabled=False
+                                ),
+                                dbc.Button(
+                                    id="btn-sel-remove",
+                                    children="Remove Selected",
+                                    color="secondary",
+                                    disabled=False
+                                ),
+                                dbc.Button(
+                                    id="btn-sel-display",
+                                    children="Display",
+                                    color="secondary",
+                                    disabled=False
+                                )
+                            ],
+                            size="md",
+                            class_name="me-1",
+                        ),
+                    ]
                 )
             ]
         )
                 )
             ]
         )
@@ -376,13 +477,9 @@ class Layout:
             return options, disable
 
         @app.callback(
             return options, disable
 
         @app.callback(
-            Output("div-ctrl-core", "style"),
             Output("cl-ctrl-core", "options"),
             Output("cl-ctrl-core", "options"),
-            Output("div-ctrl-framesize", "style"),
             Output("cl-ctrl-framesize", "options"),
             Output("cl-ctrl-framesize", "options"),
-            Output("div-ctrl-testtype", "style"),
             Output("cl-ctrl-testtype", "options"),
             Output("cl-ctrl-testtype", "options"),
-            Output("btn-ctrl-add", "disabled"),
             State("dd-ctrl-phy", "value"),
             State("dd-ctrl-area", "value"),
             Input("dd-ctrl-test", "value"),
             State("dd-ctrl-phy", "value"),
             State("dd-ctrl-area", "value"),
             Input("dd-ctrl-test", "value"),
@@ -394,36 +491,27 @@ class Layout:
             if test is None:
                 raise PreventUpdate
 
             if test is None:
                 raise PreventUpdate
 
-            core_style = {"display": "none"}
             core_opts = []
             core_opts = []
-            framesize_style = {"display": "none"}
             framesize_opts = []
             framesize_opts = []
-            testtype_style = {"display": "none"}
             testtype_opts = []
             testtype_opts = []
-            add_disabled = True
             if phy and area and test:
             if phy and area and test:
-                core_style = {"display": "block"}
                 core_opts = [
                     {"label": v, "value": v}
                         for v in self.spec_tbs[phy][area]["core"]
                 ]
                 core_opts = [
                     {"label": v, "value": v}
                         for v in self.spec_tbs[phy][area]["core"]
                 ]
-                framesize_style = {"display": "block"}
                 framesize_opts = [
                     {"label": v, "value": v}
                         for v in self.spec_tbs[phy][area]["frame-size"]
                 ]
                 framesize_opts = [
                     {"label": v, "value": v}
                         for v in self.spec_tbs[phy][area]["frame-size"]
                 ]
-                testtype_style = {"display": "block"}
                 testtype_opts = [
                     {"label": v, "value": v}
                         for v in self.spec_tbs[phy][area]["test-type"]
                 ]
                 testtype_opts = [
                     {"label": v, "value": v}
                         for v in self.spec_tbs[phy][area]["test-type"]
                 ]
-                add_disabled = False
 
             return (
 
             return (
-                core_style, core_opts,
-                framesize_style, framesize_opts,
-                testtype_style, testtype_opts,
-                add_disabled
+                core_opts,
+                framesize_opts,
+                testtype_opts,
             )
 
         def _sync_checklists(opt, sel, all, id):
             )
 
         def _sync_checklists(opt, sel, all, id):
@@ -471,13 +559,13 @@ class Layout:
             return _sync_checklists(opt, sel, all, "cl-ctrl-testtype")
 
         @app.callback(
             return _sync_checklists(opt, sel, all, "cl-ctrl-testtype")
 
         @app.callback(
-            Output("graph", "figure"),
+            Output("graph-tput", "figure"),
+            Output("graph-latency", "figure"),
             Output("selected-tests", "data"),  # Store
             Output("cl-selected", "options"),  # User selection
             Output("dd-ctrl-phy", "value"),
             Output("dd-ctrl-area", "value"),
             Output("dd-ctrl-test", "value"),
             Output("selected-tests", "data"),  # Store
             Output("cl-selected", "options"),  # User selection
             Output("dd-ctrl-phy", "value"),
             Output("dd-ctrl-area", "value"),
             Output("dd-ctrl-test", "value"),
-            Output("div-plotting-area", "style"),
             State("selected-tests", "data"),  # Store
             State("cl-selected", "value"),
             State("dd-ctrl-phy", "value"),
             State("selected-tests", "data"),  # Store
             State("cl-selected", "value"),
             State("dd-ctrl-phy", "value"),
@@ -489,17 +577,19 @@ class Layout:
             Input("btn-ctrl-add", "n_clicks"),
             Input("btn-sel-display", "n_clicks"),
             Input("btn-sel-remove", "n_clicks"),
             Input("btn-ctrl-add", "n_clicks"),
             Input("btn-sel-display", "n_clicks"),
             Input("btn-sel-remove", "n_clicks"),
+            Input("btn-sel-remove-all", "n_clicks"),
             Input("dpr-period", "start_date"),
             Input("dpr-period", "end_date"),
             prevent_initial_call=True
         )
         def _process_list(store_sel, list_sel, phy, area, test, cores,
                 framesizes, testtypes, btn_add, btn_display, btn_remove,
             Input("dpr-period", "start_date"),
             Input("dpr-period", "end_date"),
             prevent_initial_call=True
         )
         def _process_list(store_sel, list_sel, phy, area, test, cores,
                 framesizes, testtypes, btn_add, btn_display, btn_remove,
-                d_start, d_end):
+                btn_remove_all, d_start, d_end):
             """
             """
 
             """
             """
 
-            if not (btn_add or btn_display or btn_remove or d_start or d_end):
+            if not (btn_add or btn_display or btn_remove or btn_remove_all or \
+                    d_start or d_end):
                 raise PreventUpdate
 
             def _list_tests():
                 raise PreventUpdate
 
             def _list_tests():
@@ -511,24 +601,42 @@ class Layout:
                 else:
                     return list()
 
                 else:
                     return list()
 
+            class RetunValue:
+                def __init__(self) -> None:
+                    self._output = {
+                        "graph-tput-figure": no_update,
+                        "graph-lat-figure": no_update,
+                        "selected-tests-data": no_update,
+                        "cl-selected-options": no_update,
+                        "dd-ctrl-phy-value": no_update,
+                        "dd-ctrl-area-value": no_update,
+                        "dd-ctrl-test-value": no_update,
+                    }
+
+                def value(self):
+                    return tuple(self._output.values())
+
+                def set_values(self, kwargs: dict) -> None:
+                    for key, val in kwargs.items():
+                        if key in self._output:
+                            self._output[key] = val
+                        else:
+                            raise KeyError(f"The key {key} is not defined.")
+
+
             trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
 
             trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
 
-            d_start = datetime(
-                 int(d_start[0:4]), int(d_start[5:7]), int(d_start[8:10])
-            )
-            d_end = datetime(
-                 int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10])
-            )
+            d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
+                int(d_start[8:10]))
+            d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
+
+            output = RetunValue()
 
             if trigger_id == "btn-ctrl-add":
                 # Add selected test to the list of tests in store:
                 if phy and area and test and cores and framesizes and testtypes:
 
             if trigger_id == "btn-ctrl-add":
                 # Add selected test to the list of tests in store:
                 if phy and area and test and cores and framesizes and testtypes:
-
-                    # TODO: Add validation
-
                     if store_sel is None:
                         store_sel = list()
                     if store_sel is None:
                         store_sel = list()
-
                     for core in cores:
                         for framesize in framesizes:
                             for ttype in testtypes:
                     for core in cores:
                         for framesize in framesizes:
                             for ttype in testtypes:
@@ -550,16 +658,31 @@ class Layout:
                                         "core": core.lower(),
                                         "testtype": ttype.lower()
                                     })
                                         "core": core.lower(),
                                         "testtype": ttype.lower()
                                     })
-                return (no_update, store_sel, _list_tests(), None,
-                    None, None, no_update)
+                output.set_values({
+                    "selected-tests-data": store_sel,
+                    "cl-selected-options": _list_tests(),
+                    "dd-ctrl-phy-value": None,
+                    "dd-ctrl-area-value": None,
+                    "dd-ctrl-test-value": None,
+                })
 
             elif trigger_id in ("btn-sel-display", "dpr-period"):
 
             elif trigger_id in ("btn-sel-display", "dpr-period"):
-                fig, style = trending_tput(
+                fig_tput, fig_lat = graph_trending(
                     self.data, store_sel, self.layout, d_start, d_end
                 )
                     self.data, store_sel, self.layout, d_start, d_end
                 )
-                return (fig, no_update, no_update,
-                    no_update, no_update, no_update, style)
-
+                output.set_values({
+                    "graph-tput-figure": \
+                        fig_tput if fig_tput else self.NO_GRAPH,
+                    "graph-lat-figure": \
+                        fig_lat if fig_lat else self.NO_GRAPH,
+                })
+            elif trigger_id == "btn-sel-remove-all":
+                output.set_values({
+                    "graph-tput-figure": self.NO_GRAPH,
+                    "graph-lat-figure": self.NO_GRAPH,
+                    "selected-tests-data": list(),
+                    "cl-selected-options": list()
+                })
             elif trigger_id == "btn-sel-remove":
                 if list_sel:
                     new_store_sel = list()
             elif trigger_id == "btn-sel-remove":
                 if list_sel:
                     new_store_sel = list()
@@ -568,26 +691,96 @@ class Layout:
                             new_store_sel.append(item)
                     store_sel = new_store_sel
                 if store_sel:
                             new_store_sel.append(item)
                     store_sel = new_store_sel
                 if store_sel:
-                    fig, style = trending_tput(
+                    fig_tput, fig_lat = graph_trending(
                         self.data, store_sel, self.layout, d_start, d_end
                     )
                         self.data, store_sel, self.layout, d_start, d_end
                     )
-                    return (fig, store_sel, _list_tests(),
-                    no_update, no_update, no_update, style)
+                    output.set_values({
+                        "graph-tput-figure": \
+                            fig_tput if fig_tput else self.NO_GRAPH,
+                        "graph-lat-figure": \
+                            fig_lat if fig_lat else self.NO_GRAPH,
+                        "selected-tests-data": store_sel,
+                        "cl-selected-options": _list_tests()
+                    })
                 else:
                 else:
-                    style={
-                        "vertical-align": "top",
-                        "display": "none",
-                        "width": "80%",
-                        "padding": "5px"
-                    }
-                    return (no_update, store_sel, _list_tests(),
-                        no_update, no_update, no_update, style)
+                    output.set_values({
+                        "graph-tput-figure": self.NO_GRAPH,
+                        "graph-lat-figure": self.NO_GRAPH,
+                        "selected-tests-data": store_sel,
+                        "cl-selected-options": _list_tests()
+                    })
+
+            return output.value()
+
+        @app.callback(
+            Output("metadata-tput-lat", "children"),
+            Output("metadata-hdrh-graph", "children"),
+            Output("offcanvas-metadata", "is_open"),
+            Input("graph-tput", "clickData"),
+            Input("graph-latency", "clickData")
+        )
+        def _show_tput_metadata(tput_data, lat_data) -> dbc.Card:
+            """
+            """
+            if not (tput_data or lat_data):
+                raise PreventUpdate
+
+            metadata = no_update
+            graph = list()
+
+            trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
+            if trigger_id == "graph-tput":
+                title = "Throughput"
+                txt = tput_data["points"][0]["text"].replace("<br>", "\n")
+            elif trigger_id == "graph-latency":
+                title = "Latency"
+                txt = lat_data["points"][0]["text"].replace("<br>", "\n")
+                hdrh_data = lat_data["points"][0].get("customdata", None)
+                if hdrh_data:
+                    graph = [dcc.Graph(
+                        id="hdrh-latency-graph",
+                        figure=graph_hdrh_latency(hdrh_data, self.layout)
+                    ), ]
+
+            metadata = [
+                dbc.Card(
+                    children=[
+                        dbc.CardHeader(children=[
+                            dcc.Clipboard(
+                                target_id="tput-lat-metadata",
+                                title="Copy",
+                                style={"display": "inline-block"}
+                            ),
+                            title
+                        ]),
+                        dbc.CardBody(
+                            id="tput-lat-metadata",
+                            children=[txt]
+                        )
+                    ]
+                )
+            ]
+
+            return metadata, graph, True
 
         @app.callback(
 
         @app.callback(
-            Output("hover-metadata", "children"),
-            Input("graph", "clickData")
+            Output("download-data", "data"),
+            State("selected-tests", "data"),
+            Input("btn-download-data", "n_clicks"),
+            prevent_initial_call=True
         )
         )
-        def _show_metadata(hover_data):
-            if not hover_data:
+        def _download_data(store_sel, n_clicks):
+            """
+            """
+
+            if not n_clicks:
                 raise PreventUpdate
                 raise PreventUpdate
-            return json.dumps(hover_data, indent=2)
+
+            df = pd.DataFrame()
+            for itm in store_sel:
+                sel_data = select_trending_data(self.data, itm)
+                if sel_data is None:
+                    continue
+                df = pd.concat([df, sel_data], ignore_index=True)
+
+            return dcc.send_data_frame(df.to_csv, "trending_data.csv")