C-Dash: Add detailed views to comparison tables 56/40556/6
authorTibor Frank <tifrank@cisco.com>
Wed, 20 Mar 2024 05:43:03 +0000 (05:43 +0000)
committerTibor Frank <tifrank@cisco.com>
Fri, 22 Mar 2024 08:38:36 +0000 (08:38 +0000)
Change-Id: I0936f736497299f8b9fc1254012b2a0b20c41bfb
Signed-off-by: Tibor Frank <tifrank@cisco.com>
csit.infra.dash/app/cdash/comparisons/comparisons.py
csit.infra.dash/app/cdash/comparisons/layout.py
csit.infra.dash/app/cdash/comparisons/tables.py
csit.infra.dash/app/cdash/report/graphs.py
csit.infra.dash/app/cdash/search/layout.py
csit.infra.dash/app/cdash/utils/utils.py

index 5700552..f2cda81 100644 (file)
@@ -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
index d325426..3aa3239 100644 (file)
@@ -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
index 18f9404..0e32f38 100644 (file)
@@ -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):
index 44c57d4..0627411 100644 (file)
 """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()
index c803505..aa4dd53 100644 (file)
@@ -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:
index 3d2866f..692e45e 100644 (file)
@@ -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,