From 230044632667a3eb7794218a6ba3e2fa2c9b71b4 Mon Sep 17 00:00:00 2001 From: Tibor Frank Date: Wed, 20 Mar 2024 05:43:03 +0000 Subject: [PATCH] C-Dash: Add detailed views to comparison tables Change-Id: I0936f736497299f8b9fc1254012b2a0b20c41bfb Signed-off-by: Tibor Frank --- .../app/cdash/comparisons/comparisons.py | 1 + csit.infra.dash/app/cdash/comparisons/layout.py | 202 ++++++++++++++++++++- csit.infra.dash/app/cdash/comparisons/tables.py | 15 +- csit.infra.dash/app/cdash/report/graphs.py | 42 ++++- csit.infra.dash/app/cdash/search/layout.py | 29 +-- csit.infra.dash/app/cdash/utils/utils.py | 33 ++++ 6 files changed, 286 insertions(+), 36 deletions(-) diff --git a/csit.infra.dash/app/cdash/comparisons/comparisons.py b/csit.infra.dash/app/cdash/comparisons/comparisons.py index 5700552af3..f2cda8138b 100644 --- a/csit.infra.dash/app/cdash/comparisons/comparisons.py +++ b/csit.infra.dash/app/cdash/comparisons/comparisons.py @@ -44,6 +44,7 @@ def init_comparisons( app=dash_app, data_iterative=data_iterative, html_layout_file=C.HTML_LAYOUT_FILE, + graph_layout_file=C.REPORT_GRAPH_LAYOUT_FILE, tooltip_file=C.TOOLTIP_FILE ) dash_app.index_string = layout.html_layout diff --git a/csit.infra.dash/app/cdash/comparisons/layout.py b/csit.infra.dash/app/cdash/comparisons/layout.py index d32542617c..3aa32399cf 100644 --- a/csit.infra.dash/app/cdash/comparisons/layout.py +++ b/csit.infra.dash/app/cdash/comparisons/layout.py @@ -26,14 +26,16 @@ 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, navbar_report, \ - filter_table_data, show_tooltip + 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. @@ -80,12 +82,15 @@ class Layout: app: Flask, data_iterative: pd.DataFrame, 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. @@ -93,9 +98,12 @@ class Layout: 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 """ @@ -103,6 +111,7 @@ class Layout: self._app = app 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: @@ -174,6 +183,20 @@ class Layout: 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) @@ -232,6 +255,31 @@ class Layout: 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", @@ -625,7 +673,7 @@ class Layout: editable=False, filter_action="custom", filter_query="", - sort_action="native", + sort_action="custom", sort_mode="multi", selected_columns=[], selected_rows=[], @@ -749,6 +797,7 @@ class Layout: 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") @@ -763,7 +812,6 @@ class Layout: href: str, normalize: list, outliers: bool, - table_filter: str, *_ ) -> tuple: """Update the application when the event is detected. @@ -1020,10 +1068,16 @@ class Layout: "cmp-val-val": str() }) elif trigger.type == "table" and trigger.idx == "comparison": - filtered_data = filter_table_data( - store_table_data, - table_filter[0] - ) + 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"], @@ -1149,3 +1203,137 @@ class Layout: 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 diff --git a/csit.infra.dash/app/cdash/comparisons/tables.py b/csit.infra.dash/app/cdash/comparisons/tables.py index 18f9404f0a..0e32f38b6c 100644 --- a/csit.infra.dash/app/cdash/comparisons/tables.py +++ b/csit.infra.dash/app/cdash/comparisons/tables.py @@ -95,15 +95,14 @@ def select_comp_data( tmp_df.extend(l_itm) l_df = tmp_df - if remove_outliers: - q1 = percentile(l_df, 25, method=C.COMP_PERCENTILE_METHOD) - q3 = percentile(l_df, 75, method=C.COMP_PERCENTILE_METHOD) - irq = q3 - q1 - lif = q1 - C.COMP_OUTLIER_TYPE * irq - uif = q3 + C.COMP_OUTLIER_TYPE * irq - l_df = [i for i in l_df if i >= lif and i <= uif] - try: + if remove_outliers: + q1 = percentile(l_df, 25, method=C.COMP_PERCENTILE_METHOD) + q3 = percentile(l_df, 75, method=C.COMP_PERCENTILE_METHOD) + irq = q3 - q1 + lif = q1 - C.COMP_OUTLIER_TYPE * irq + uif = q3 + C.COMP_OUTLIER_TYPE * irq + l_df = [i for i in l_df if i >= lif and i <= uif] mean_val = mean(l_df) std_val = std(l_df) except (TypeError, ValueError): diff --git a/csit.infra.dash/app/cdash/report/graphs.py b/csit.infra.dash/app/cdash/report/graphs.py index 44c57d4183..0627411d0f 100644 --- a/csit.infra.dash/app/cdash/report/graphs.py +++ b/csit.infra.dash/app/cdash/report/graphs.py @@ -14,11 +14,11 @@ """Implementation of graphs for iterative data. """ - import plotly.graph_objects as go import pandas as pd from copy import deepcopy +from numpy import percentile from ..utils.constants import Constants as C from ..utils.utils import get_color, get_hdrh_latencies @@ -74,7 +74,7 @@ def select_iterative_data(data: pd.DataFrame, itm:dict) -> pd.DataFrame: def graph_iterative(data: pd.DataFrame, sel: list, layout: dict, - normalize: bool=False) -> tuple: + normalize: bool=False, remove_outliers: bool=False) -> tuple: """Generate the statistical box graph with iterative data (MRR, NDR and PDR, for PDR also Latencies). @@ -83,15 +83,19 @@ def graph_iterative(data: pd.DataFrame, sel: list, layout: dict, :param layout: Layout of plot.ly graph. :param normalize: If True, the data is normalized to CPU frequency Constants.NORM_FREQUENCY. - :param data: pandas.DataFrame - :param sel: list - :param layout: dict - :param normalize: bool + :param remove_outliers: If True the outliers are removed before + generating the table. + :type data: pandas.DataFrame + :type sel: list + :type layout: dict + :type normalize: bool + :type remove_outliers: bool :returns: Tuple of graphs - throughput and latency. :rtype: tuple(plotly.graph_objects.Figure, plotly.graph_objects.Figure) """ - def get_y_values(data, y_data_max, param, norm_factor, release=str()): + def get_y_values(data, y_data_max, param, norm_factor, release=str(), + remove_outliers=False): if param == "result_receive_rate_rate_values": if release == "rls2402": y_vals_raw = data["result_receive_rate_rate_avg"].to_list() @@ -100,6 +104,17 @@ def graph_iterative(data: pd.DataFrame, sel: list, layout: dict, else: y_vals_raw = data[param].to_list() y_data = [(y * norm_factor) for y in y_vals_raw] + + if remove_outliers: + try: + q1 = percentile(y_data, 25, method=C.COMP_PERCENTILE_METHOD) + q3 = percentile(y_data, 75, method=C.COMP_PERCENTILE_METHOD) + irq = q3 - q1 + lif = q1 - C.COMP_OUTLIER_TYPE * irq + uif = q3 + C.COMP_OUTLIER_TYPE * irq + y_data = [i for i in y_data if i >= lif and i <= uif] + except TypeError: + pass try: y_data_max = max(max(y_data), y_data_max) except TypeError: @@ -142,7 +157,12 @@ def graph_iterative(data: pd.DataFrame, sel: list, layout: dict, y_units.update(itm_data[C.UNIT[ttype]].unique().tolist()) y_data, y_tput_max = get_y_values( - itm_data, y_tput_max, C.VALUE_ITER[ttype], norm_factor, itm["rls"] + itm_data, + y_tput_max, + C.VALUE_ITER[ttype], + norm_factor, + itm["rls"], + remove_outliers ) nr_of_samples = len(y_data) @@ -192,7 +212,8 @@ def graph_iterative(data: pd.DataFrame, sel: list, layout: dict, itm_data, y_band_max, C.VALUE_ITER[f"{ttype}-bandwidth"], - norm_factor + norm_factor, + remove_outliers=remove_outliers ) if not all(pd.isna(y_band)): y_band_units.update( @@ -221,7 +242,8 @@ def graph_iterative(data: pd.DataFrame, sel: list, layout: dict, itm_data, y_lat_max, C.VALUE_ITER["latency"], - 1 / norm_factor + 1 / norm_factor, + remove_outliers=remove_outliers ) if not all(pd.isna(y_lat)): customdata = list() diff --git a/csit.infra.dash/app/cdash/search/layout.py b/csit.infra.dash/app/cdash/search/layout.py index c8035055e4..aa4dd53d5b 100644 --- a/csit.infra.dash/app/cdash/search/layout.py +++ b/csit.infra.dash/app/cdash/search/layout.py @@ -32,8 +32,8 @@ from ..utils.constants import Constants as C from ..utils.control_panel import ControlPanel from ..utils.trigger import Trigger from ..utils.utils import gen_new_url, generate_options, navbar_trending, \ - filter_table_data, show_trending_graph_data, show_iterative_graph_data, \ - show_tooltip + filter_table_data, sort_table_data, show_trending_graph_data, \ + show_iterative_graph_data, show_tooltip from ..utils.url_processing import url_decode from .tables import search_table from ..coverage.tables import coverage_tables @@ -448,7 +448,7 @@ class Layout: columns=columns, data=table.to_dict("records"), filter_action="custom", - sort_action="native", + sort_action="custom", sort_mode="multi", selected_columns=[], selected_rows=[], @@ -538,6 +538,7 @@ class Layout: State({"type": "table", "index": ALL}, "data"), Input("url", "href"), Input({"type": "table", "index": ALL}, "filter_query"), + Input({"type": "table", "index": ALL}, "sort_by"), Input({"type": "ctrl-dd", "index": ALL}, "value"), prevent_initial_call=True ) @@ -679,10 +680,16 @@ class Layout: } on_draw = True elif trigger.type == "table" and trigger.idx == "search": - filtered_data = filter_table_data( - store_table_data, - trigger.value - ) + 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 on_draw: @@ -735,8 +742,8 @@ class Layout: rls = store["selection"]["release"] tb = row["Test Bed"].iloc[0] nic = row["NIC"].iloc[0] - driver = row['Driver'].iloc[0] - test_name = row['Test'].iloc[0] + driver = row["Driver"].iloc[0] + test_name = row["Test"].iloc[0] dutver = str() except(KeyError, IndexError, AttributeError, ValueError): raise PreventUpdate @@ -777,7 +784,7 @@ class Layout: testtype = [testtype, ] core = l_test[1] if l_test[1] else "8c" test = "-".join(l_test[2: -1]) - test_id = f"{tb}-{nic}-{driver}-{core}-{l_test[0]}-{test}" + test_id = f"{tb}-{nic}-{driver}-{l_test[0]}-{core}-{test}" title = dbc.Row( class_name="g-0 p-0", children=dbc.Alert(test_id, color="info"), @@ -873,7 +880,7 @@ class Layout: Input({"type": "graph-iter", "index": ALL}, "clickData"), prevent_initial_call=True ) - def _show_metadata_from_trend_graph( + def _show_metadata_from_graph( trend_data: dict, iter_data: dict ) -> tuple: diff --git a/csit.infra.dash/app/cdash/utils/utils.py b/csit.infra.dash/app/cdash/utils/utils.py index 3d2866fbe0..692e45efb2 100644 --- a/csit.infra.dash/app/cdash/utils/utils.py +++ b/csit.infra.dash/app/cdash/utils/utils.py @@ -631,6 +631,39 @@ def filter_table_data( return df.to_dict("records") +def sort_table_data( + store_table_data: list, + sort_by: list + ) -> list: + """Sort table data using user specified order. + + :param store_table_data: Table data represented as a list of records. + :param sort_by: User specified sorting order (multicolumn). + :type store_table_data: list + :type sort_by: list + :returns: A new table created by sorting the table data represented as + a list of records. + :rtype: list + """ + + # Checks: + if not any((sort_by, store_table_data, )): + return store_table_data + + df = pd.DataFrame.from_records(store_table_data) + if len(sort_by): + dff = df.sort_values( + [col["column_id"] for col in sort_by], + ascending=[col["direction"] == "asc" for col in sort_by], + inplace=False + ) + else: + # No sort is applied + dff = df + + return dff.to_dict("records") + + def show_trending_graph_data( trigger: Trigger, data: dict, -- 2.16.6