X-Git-Url: https://gerrit.fd.io/r/gitweb?a=blobdiff_plain;f=csit.infra.dash%2Fapp%2Fcdash%2Freport%2Flayout.py;h=1978e7abae798682dd15edfb354e37097b4835be;hb=540b27dbf9befcf589f5f572e8aac909f1738b51;hp=6f400195830ff51b19ddb00217a93f8eb93ed5df;hpb=a5836196e06db97aa369efdd3b160104eb6ae1f8;p=csit.git diff --git a/csit.infra.dash/app/cdash/report/layout.py b/csit.infra.dash/app/cdash/report/layout.py index 6f40019583..1978e7abae 100644 --- a/csit.infra.dash/app/cdash/report/layout.py +++ b/csit.infra.dash/app/cdash/report/layout.py @@ -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,10 +32,10 @@ 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, get_short_version, select_iterative_data +from .graphs import graph_iterative, select_iterative_data # Control panel partameters and their default values. @@ -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,60 +93,41 @@ 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.replace("csit", "rls")) - data_mrr["release"] = rls - data_ndrpdr = Data(self._data_spec_file, True).\ - read_iterative_ndrpdr(release=rls.replace("csit", "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 = get_short_version(row["dut_version"], dut) + d_ver = row["dut_version"] tbed = "-".join(lst_job[-2:]) lst_test_id = row["test_id"].split(".") 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", @@ -370,13 +354,11 @@ class Layout: children=[ dbc.InputGroup( [ - dbc.InputGroupText( - children=show_tooltip( - self._tooltips, - "help-release", - "CSIT Release" - ) - ), + dbc.InputGroupText(show_tooltip( + self._tooltips, + "help-release", + "CSIT Release" + )), dbc.Select( id={"type": "ctrl-dd", "index": "rls"}, placeholder="Select a Release...", @@ -398,13 +380,11 @@ class Layout: children=[ dbc.InputGroup( [ - dbc.InputGroupText( - children=show_tooltip( - self._tooltips, - "help-dut", - "DUT" - ) - ), + dbc.InputGroupText(show_tooltip( + self._tooltips, + "help-dut", + "DUT" + )), dbc.Select( id={"type": "ctrl-dd", "index": "dut"}, placeholder="Select a Device under Test..." @@ -419,13 +399,11 @@ class Layout: children=[ dbc.InputGroup( [ - dbc.InputGroupText( - children=show_tooltip( - self._tooltips, - "help-dut-ver", - "DUT Version" - ) - ), + dbc.InputGroupText(show_tooltip( + self._tooltips, + "help-dut-ver", + "DUT Version" + )), dbc.Select( id={"type": "ctrl-dd", "index": "dutver"}, placeholder=\ @@ -441,17 +419,14 @@ class Layout: children=[ dbc.InputGroup( [ - dbc.InputGroupText( - children=show_tooltip( - self._tooltips, - "help-infra", - "Infra" - ) - ), + dbc.InputGroupText(show_tooltip( + self._tooltips, + "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" @@ -463,16 +438,14 @@ class Layout: children=[ dbc.InputGroup( [ - dbc.InputGroupText( - children=show_tooltip( - self._tooltips, - "help-area", - "Area" - ) - ), + dbc.InputGroupText(show_tooltip( + self._tooltips, + "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" @@ -484,16 +457,15 @@ class Layout: children=[ dbc.InputGroup( [ - dbc.InputGroupText( - children=show_tooltip( - self._tooltips, - "help-test", - "Test" - ) - ), + dbc.InputGroupText(show_tooltip( + self._tooltips, + "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 +655,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 +723,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 +879,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 +901,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 +1024,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 +1055,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 +1088,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 +1120,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, @@ -1190,7 +1163,7 @@ class Layout: f"cl-{param}-val": val_sel, f"cl-{param}-all-val": val_all, }) - if all((ctrl_panel.get("cl-core-val"), + if all((ctrl_panel.get("cl-core-val"), ctrl_panel.get("cl-frmsize-val"), ctrl_panel.get("cl-tsttype-val"), )): ctrl_panel.set({"btn-add-dis": False}) @@ -1251,7 +1224,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 +1256,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 +1273,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 +1295,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