-# Copyright (c) 2023 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:
"""Plotly Dash HTML layout override.
"""
+
+import logging
import pandas as pd
import dash_bootstrap_components as dbc
from dash.exceptions import PreventUpdate
from dash.dash_table.Format import Format, Scheme
from ast import literal_eval
+from yaml import load, FullLoader, YAMLError
+from copy import deepcopy
from ..utils.constants import Constants as C
from ..utils.control_panel import ControlPanel
from ..utils.trigger import Trigger
from ..utils.url_processing import url_decode
-from ..utils.utils import generate_options, gen_new_url
-from .tables import comparison_table, filter_table_data
+from ..utils.utils import generate_options, gen_new_url, navbar_report, \
+ filter_table_data, sort_table_data, show_iterative_graph_data, show_tooltip
+from .tables import comparison_table
+from ..report.graphs import graph_iterative
# Control panel partameters and their default values.
"cmp-val-opt": list(),
"cmp-val-dis": True,
"cmp-val-val": str(),
- "normalize-val": list()
+ "normalize-val": list(),
+ "outliers-val": list()
}
# List of comparable parameters.
self,
app: Flask,
data_iterative: pd.DataFrame,
- html_layout_file: str
+ html_layout_file: str,
+ graph_layout_file: str,
+ tooltip_file: str
) -> None:
"""Initialization:
- save the input parameters,
- prepare data for the control panel,
- read HTML layout file,
+ - read graph layout file,
+ - read tooltips from the tooltip file.
:param app: Flask application running the dash application.
:param data_iterative: Iterative data to be used in comparison tables.
:param html_layout_file: Path and name of the file specifying the HTML
layout of the dash application.
+ :param tooltip_file: Path and name of the yaml file specifying the
+ tooltips.
+ :param graph_layout_file: Path and name of the file with layout of
+ plot.ly graphs.
:type app: Flask
:type data_iterative: pandas.DataFrame
:type html_layout_file: str
+ :type graph_layout_file: str
+ :type tooltip_file: str
"""
# Inputs
self._app = app
- self._html_layout_file = html_layout_file
self._data = data_iterative
+ self._html_layout_file = html_layout_file
+ self._graph_layout_file = graph_layout_file
+ self._tooltip_file = tooltip_file
# Get structure of tests:
tbs = dict()
f"Not possible to open the file {self._html_layout_file}\n{err}"
)
+ try:
+ with open(self._graph_layout_file, "r") as file_read:
+ self._graph_layout = load(file_read, Loader=FullLoader)
+ except IOError as err:
+ raise RuntimeError(
+ f"Not possible to open the file {self._graph_layout_file}\n"
+ f"{err}"
+ )
+ except YAMLError as err:
+ raise RuntimeError(
+ f"An error occurred while parsing the specification file "
+ f"{self._graph_layout_file}\n{err}"
+ )
+
+ try:
+ with open(self._tooltip_file, "r") as file_read:
+ self._tooltips = load(file_read, Loader=FullLoader)
+ except IOError as err:
+ logging.warning(
+ f"Not possible to open the file {self._tooltip_file}\n{err}"
+ )
+ except YAMLError as err:
+ logging.warning(
+ f"An error occurred while parsing the specification file "
+ f"{self._tooltip_file}\n{err}"
+ )
+
# Callbacks:
if self._app is not None and hasattr(self, "callbacks"):
self.callbacks(self._app)
dbc.Row(
id="row-navbar",
class_name="g-0",
- children=[
- self._add_navbar()
- ]
+ children=[navbar_report((False, True, False, False)), ]
),
dbc.Row(
id="row-main",
self._add_ctrl_col(),
self._add_plotting_col()
]
+ ),
+ dbc.Spinner(
+ dbc.Offcanvas(
+ class_name="w-75",
+ id="offcanvas-details",
+ title="Test Details",
+ placement="end",
+ is_open=False,
+ children=[]
+ ),
+ delay_show=C.SPINNER_DELAY
+ ),
+ dbc.Spinner(
+ dbc.Offcanvas(
+ class_name="w-50",
+ id="offcanvas-metadata",
+ title="Detailed Information",
+ 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%"
+ )
)
]
)
]
)
- 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.COMP_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.
children=[
dbc.InputGroup(
[
- dbc.InputGroupText("DUT"),
+ dbc.InputGroupText(
+ show_tooltip(self._tooltips, "help-dut", "DUT")
+ ),
dbc.Select(
id={"type": "ctrl-dd", "index": "dut"},
placeholder="Select a Device under Test...",
children=[
dbc.InputGroup(
[
- dbc.InputGroupText("CSIT and DUT Version"),
+ dbc.InputGroupText(show_tooltip(
+ self._tooltips,
+ "help-csit-dut",
+ "CSIT and DUT Version"
+ )),
dbc.Select(
id={"type": "ctrl-dd", "index": "dutver"},
placeholder="Select a CSIT and DUT Version...")
children=[
dbc.InputGroup(
[
- dbc.InputGroupText("Infra"),
+ dbc.InputGroupText(show_tooltip(
+ self._tooltips,
+ "help-infra",
+ "Infra"
+ )),
dbc.Select(
id={"type": "ctrl-dd", "index": "infra"},
placeholder=\
children=[
dbc.InputGroup(
[
- dbc.InputGroupText("Frame Size"),
+ dbc.InputGroupText(show_tooltip(
+ self._tooltips,
+ "help-framesize",
+ "Frame Size"
+ )),
dbc.Checklist(
id={"type": "ctrl-cl", "index": "frmsize"},
inline=True,
children=[
dbc.InputGroup(
[
- dbc.InputGroupText("Number of Cores"),
+ dbc.InputGroupText(show_tooltip(
+ self._tooltips,
+ "help-cores",
+ "Number of Cores"
+ )),
dbc.Checklist(
id={"type": "ctrl-cl", "index": "core"},
inline=True,
children=[
dbc.InputGroup(
[
- dbc.InputGroupText("Measurement"),
+ dbc.InputGroupText(show_tooltip(
+ self._tooltips,
+ "help-measurement",
+ "Measurement"
+ )),
dbc.Checklist(
id={"type": "ctrl-cl", "index": "ttype"},
inline=True,
children=[
dbc.InputGroup(
[
- dbc.InputGroupText("Parameter"),
+ dbc.InputGroupText(show_tooltip(
+ self._tooltips,
+ "help-cmp-parameter",
+ "Parameter"
+ )),
dbc.Select(
id={"type": "ctrl-dd", "index": "cmpprm"},
placeholder="Select a Parameter..."
children=[
dbc.InputGroup(
[
- dbc.InputGroupText("Value"),
+ dbc.InputGroupText(show_tooltip(
+ self._tooltips,
+ "help-cmp-value",
+ "Value"
+ )),
dbc.Select(
id={"type": "ctrl-dd", "index": "cmpval"},
placeholder="Select a Value..."
)
]
- normalize = [
+ processing = [
dbc.Row(
class_name="g-0 p-1",
children=[
dbc.InputGroup(
- dbc.Checklist(
- id="normalize",
- options=[{
- "value": "normalize",
- "label": "Normalize to 2GHz CPU frequency"
- }],
- value=[],
- inline=True,
- class_name="ms-2"
- ),
+ children = [
+ dbc.Checklist(
+ id="normalize",
+ options=[{
+ "value": "normalize",
+ "label": "Normalize to 2GHz CPU frequency"
+ }],
+ value=[],
+ inline=True,
+ class_name="ms-2"
+ ),
+ dbc.Checklist(
+ id="outliers",
+ options=[{
+ "value": "outliers",
+ "label": "Remove Extreme Outliers"
+ }],
+ value=[],
+ inline=True,
+ class_name="ms-2"
+ )
+ ],
style={"align-items": "center"},
size="sm"
)
dbc.Card(
[
dbc.CardHeader(
- html.H5("Normalization")
+ html.H5("Data Manipulations")
),
dbc.CardBody(
- children=normalize,
+ children=processing,
class_name="g-0 p-0"
)
],
) -> list:
"""Generate the plotting area with all its content.
- :param title: The title of the comparison table..
+ :param title: The title of the comparison table.
:param table: Comparison table to be displayed.
:param url: URL to be displayed in the modal window.
:type title: str
editable=False,
filter_action="custom",
filter_query="",
- sort_action="native",
+ sort_action="custom",
sort_mode="multi",
selected_columns=[],
selected_rows=[],
),
dbc.Button(
id="plot-btn-download",
- children="Download Data",
+ children="Download Table",
+ class_name="me-1",
+ color="info",
+ style={
+ "text-transform": "none",
+ "padding": "0rem 1rem"
+ }
+ ),
+ dcc.Download(id="download-iterative-data"),
+ dbc.Button(
+ id="plot-btn-download-raw",
+ children="Download Raw Data",
class_name="me-1",
color="info",
style={
"padding": "0rem 1rem"
}
),
- dcc.Download(id="download-iterative-data")
+ dcc.Download(id="download-raw-data")
],
className=\
"d-grid gap-0 d-md-flex justify-content-md-end"
Output({"type": "ctrl-dd", "index": "cmpval"}, "options"),
Output({"type": "ctrl-dd", "index": "cmpval"}, "disabled"),
Output({"type": "ctrl-dd", "index": "cmpval"}, "value"),
- Output("normalize", "value")
+ Output("normalize", "value"),
+ Output("outliers", "value")
],
[
State("store-control-panel", "data"),
State("store-selected", "data"),
- State("store-table-data", "data")
+ State("store-table-data", "data"),
+ State("store-filtered-table-data", "data"),
+ State({"type": "table", "index": ALL}, "data")
],
[
Input("url", "href"),
Input("normalize", "value"),
+ Input("outliers", "value"),
Input({"type": "table", "index": ALL}, "filter_query"),
+ Input({"type": "table", "index": ALL}, "sort_by"),
Input({"type": "ctrl-dd", "index": ALL}, "value"),
Input({"type": "ctrl-cl", "index": ALL}, "value"),
Input({"type": "ctrl-btn", "index": ALL}, "n_clicks")
control_panel: dict,
selected: dict,
store_table_data: list,
+ filtered_data: list,
+ table_data: list,
href: str,
normalize: list,
- table_filter: str,
+ outliers: bool,
*_
) -> tuple:
"""Update the application when the event is detected.
on_draw = False
plotting_area = no_update
- table_data = list()
- filtered_data = None
trigger = Trigger(callback_context.triggered)
if trigger.type == "url" and url_params:
r_sel = selected["reference"]["selection"]
c_sel = selected["compare"]
normalize = literal_eval(url_params["norm"][0])
+ try: # Necessary for backward compatibility
+ outliers = literal_eval(url_params["outliers"][0])
+ except (KeyError, IndexError, AttributeError):
+ outliers = list()
process_url = bool(
(selected["reference"]["set"] == True) and
(c_sel["set"] == True)
)
- except (KeyError, IndexError):
+ except (KeyError, IndexError, AttributeError):
pass
if process_url:
ctrl_panel.set({
[r_sel["infra"]]["ttype"]
),
"ttype-val": r_sel["ttype"],
- "normalize-val": normalize
+ "normalize-val": normalize,
+ "outliers-val": outliers
})
opts = list()
for itm, label in CMP_PARAMS.items():
elif trigger.type == "normalize":
ctrl_panel.set({"normalize-val": normalize})
on_draw = True
+ elif trigger.type == "outliers":
+ ctrl_panel.set({"outliers-val": outliers})
+ on_draw = True
elif trigger.type == "ctrl-dd":
if trigger.idx == "dut":
try:
"cmp-val-val": str()
})
elif trigger.type == "table" and trigger.idx == "comparison":
- table_data = filter_table_data(
- store_table_data,
- table_filter[0]
- )
- filtered_data = table_data
- table_data = [table_data, ]
+ if trigger.parameter == "filter_query":
+ filtered_data = filter_table_data(
+ store_table_data,
+ trigger.value
+ )
+ elif trigger.parameter == "sort_by":
+ filtered_data = sort_table_data(
+ store_table_data,
+ trigger.value
+ )
+ table_data = [filtered_data, ]
if all((on_draw, selected["reference"]["set"],
selected["compare"]["set"], )):
- title, table = comparison_table(self._data, selected, normalize)
+ title, table = comparison_table(
+ data=self._data,
+ selected=selected,
+ normalize=normalize,
+ format="html",
+ remove_outliers=outliers
+ )
plotting_area = self._get_plotting_area(
title=title,
table=table,
url=gen_new_url(
parsed_url,
- params={"selected": selected, "norm": normalize}
+ params={
+ "selected": selected,
+ "norm": normalize,
+ "outliers": outliers
+ }
)
)
store_table_data = table.to_dict("records")
+ filtered_data = store_table_data
+ if table_data:
+ table_data = [store_table_data, ]
ret_val = [
ctrl_panel.panel,
if not table_data:
raise PreventUpdate
-
+
if filtered_table_data:
table = pd.DataFrame.from_records(filtered_table_data)
else:
table = pd.DataFrame.from_records(table_data)
return dcc.send_data_frame(table.to_csv, C.COMP_DOWNLOAD_FILE_NAME)
+
+ @app.callback(
+ Output("download-raw-data", "data"),
+ State("store-selected", "data"),
+ Input("plot-btn-download-raw", "n_clicks"),
+ prevent_initial_call=True
+ )
+ def _download_raw_comparison_data(selected: dict, _: int) -> dict:
+ """Download the data.
+
+ :param selected: Selected tests.
+ :type selected: dict
+ :returns: dict of data frame content (base64 encoded) and meta data
+ used by the Download component.
+ :rtype: dict
+ """
+
+ if not selected:
+ raise PreventUpdate
+
+ _, table = comparison_table(
+ data=self._data,
+ selected=selected,
+ normalize=False,
+ remove_outliers=False,
+ raw_data=True
+ )
+
+ return dcc.send_data_frame(
+ table.dropna(how="all", axis=1).to_csv,
+ f"raw_{C.COMP_DOWNLOAD_FILE_NAME}"
+ )
+
+ @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
+
+ @app.callback(
+ Output("offcanvas-details", "is_open"),
+ Output("offcanvas-details", "children"),
+ State("store-selected", "data"),
+ State("store-filtered-table-data", "data"),
+ State("normalize", "value"),
+ State("outliers", "value"),
+ Input({"type": "table", "index": ALL}, "active_cell"),
+ prevent_initial_call=True
+ )
+ def show_test_data(cp_sel, table, normalize, outliers, *_):
+ """Show offcanvas with graphs and tables based on selected test(s).
+ """
+
+ trigger = Trigger(callback_context.triggered)
+ if not all((trigger.value, cp_sel["reference"]["set"], \
+ cp_sel["compare"]["set"])):
+ raise PreventUpdate
+
+ try:
+ test_name = pd.DataFrame.from_records(table).\
+ iloc[[trigger.value["row"]]]["Test Name"].iloc[0]
+ dut = cp_sel["reference"]["selection"]["dut"]
+ rls, dutver = cp_sel["reference"]["selection"]["dutver"].\
+ split("-", 1)
+ phy = cp_sel["reference"]["selection"]["infra"]
+ framesize, core, test_id = test_name.split("-", 2)
+ test, ttype = test_id.rsplit("-", 1)
+ ttype = "pdr" if ttype == "latency" else ttype
+ l_phy = phy.split("-")
+ tb = "-".join(l_phy[:2])
+ nic = l_phy[2]
+ stype = "ndrpdr" if ttype in ("ndr", "pdr") else ttype
+ except(KeyError, IndexError, AttributeError, ValueError):
+ raise PreventUpdate
+
+ df = pd.DataFrame(self._data.loc[(
+ (self._data["dut_type"] == dut) &
+ (self._data["dut_version"] == dutver) &
+ (self._data["release"] == rls)
+ )])
+ df = df[df.job.str.endswith(tb)]
+ df = df[df.test_id.str.contains(
+ f"{nic}.*{test}-{stype}", regex=True
+ )]
+ if df.empty:
+ raise PreventUpdate
+
+ l_test_id = df["test_id"].iloc[0].split(".")
+ area = ".".join(l_test_id[3:-2])
+
+ r_sel = {
+ "id": f"{test}-{ttype}",
+ "rls": rls,
+ "dut": dut,
+ "dutver": dutver,
+ "phy": phy,
+ "area": area,
+ "test": test,
+ "framesize": framesize,
+ "core": core,
+ "testtype": ttype
+ }
+
+ c_sel = deepcopy(r_sel)
+ param = cp_sel["compare"]["parameter"]
+ val = cp_sel["compare"]["value"].lower()
+ if param == "dutver":
+ c_sel["rls"], c_sel["dutver"] = val.split("-", 1)
+ elif param == "ttype":
+ c_sel["id"] = f"{test}-{val}"
+ c_sel["testtype"] = val
+ elif param == "infra":
+ c_sel["phy"] = val
+ else:
+ c_sel[param] = val
+
+ r_sel["id"] = "-".join(
+ (r_sel["phy"], r_sel["framesize"], r_sel["core"], r_sel["id"])
+ )
+ c_sel["id"] = "-".join(
+ (c_sel["phy"], c_sel["framesize"], c_sel["core"], c_sel["id"])
+ )
+ selected = [r_sel, c_sel]
+
+ indexes = ("tput", "bandwidth", "lat")
+ graphs = graph_iterative(
+ self._data,
+ selected,
+ self._graph_layout,
+ bool(normalize),
+ bool(outliers)
+ )
+ cols = list()
+ for graph, idx in zip(graphs, indexes):
+ if graph:
+ cols.append(dbc.Col(dcc.Graph(
+ figure=graph,
+ id={"type": "graph-iter", "index": idx},
+ )))
+ if not cols:
+ cols="No data."
+ ret_val = [
+ dbc.Row(
+ class_name="g-0 p-0",
+ children=dbc.Alert(test, color="info"),
+ ),
+ dbc.Row(class_name="g-0 p-0", children=cols)
+ ]
+
+ return True, ret_val
+
+ @app.callback(
+ Output("metadata-tput-lat", "children"),
+ Output("metadata-hdrh-graph", "children"),
+ Output("offcanvas-metadata", "is_open"),
+ Input({"type": "graph-iter", "index": ALL}, "clickData"),
+ prevent_initial_call=True
+ )
+ def _show_metadata_from_graph(iter_data: dict) -> tuple:
+ """Generates the data for the offcanvas displayed when a particular
+ point in a graph is clicked on.
+ """
+
+ trigger = Trigger(callback_context.triggered)
+ if not trigger.value:
+ raise PreventUpdate
+
+ if trigger.type == "graph-iter":
+ return show_iterative_graph_data(
+ trigger, iter_data, self._graph_layout)
+ else:
+ raise PreventUpdate