1 # Copyright (c) 2024 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
6 # http://www.apache.org/licenses/LICENSE-2.0
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
14 """Plotly Dash HTML layout override.
20 import dash_bootstrap_components as dbc
22 from flask import Flask
23 from dash import dcc, html, dash_table, callback_context, no_update, ALL
24 from dash import Input, Output, State
25 from dash.exceptions import PreventUpdate
26 from dash.dash_table.Format import Format, Scheme
27 from ast import literal_eval
28 from yaml import load, FullLoader, YAMLError
29 from copy import deepcopy
31 from ..utils.constants import Constants as C
32 from ..utils.control_panel import ControlPanel
33 from ..utils.trigger import Trigger
34 from ..utils.url_processing import url_decode
35 from ..utils.utils import generate_options, gen_new_url, navbar_report, \
36 filter_table_data, sort_table_data, show_iterative_graph_data, show_tooltip
37 from .tables import comparison_table
38 from ..report.graphs import graph_iterative
41 # Control panel partameters and their default values.
52 "frmsize-opt": list(),
53 "frmsize-val": list(),
56 "cmp-par-opt": list(),
59 "cmp-val-opt": list(),
62 "normalize-val": list(),
63 "outliers-val": list()
66 # List of comparable parameters.
68 "dutver": "Release and Version",
69 "infra": "Infrastructure",
70 "frmsize": "Frame Size",
71 "core": "Number of Cores",
72 "ttype": "Measurement"
77 """The layout of the dash app and the callbacks.
83 data_iterative: pd.DataFrame,
84 html_layout_file: str,
85 graph_layout_file: str,
89 - save the input parameters,
90 - prepare data for the control panel,
91 - read HTML layout file,
92 - read graph layout file,
93 - read tooltips from the tooltip file.
95 :param app: Flask application running the dash application.
96 :param data_iterative: Iterative data to be used in comparison tables.
97 :param html_layout_file: Path and name of the file specifying the HTML
98 layout of the dash application.
99 :param tooltip_file: Path and name of the yaml file specifying the
101 :param graph_layout_file: Path and name of the file with layout of
104 :type data_iterative: pandas.DataFrame
105 :type html_layout_file: str
106 :type graph_layout_file: str
107 :type tooltip_file: str
112 self._data = data_iterative
113 self._html_layout_file = html_layout_file
114 self._graph_layout_file = graph_layout_file
115 self._tooltip_file = tooltip_file
117 # Get structure of tests:
120 "job", "test_id", "test_type", "dut_type", "dut_version", "tg_type",
123 for _, row in self._data[cols].drop_duplicates().iterrows():
124 lst_job = row["job"].split("-")
126 dver = f"{row['release']}-{row['dut_version']}"
127 tbed = "-".join(lst_job[-2:])
128 lst_test_id = row["test_id"].split(".")
130 suite = lst_test_id[-2].replace("2n1l-", "").replace("1n1l-", "").\
132 test = lst_test_id[-1]
133 nic = suite.split("-")[0]
134 for driver in C.DRIVERS:
136 drv = driver.replace("-", "_")
137 test = test.replace(f"{driver}-", "")
141 infra = "-".join((tbed, nic, drv))
142 lst_test = test.split("-")
144 core = lst_test[1] if lst_test[1] else "8C"
146 if tbs.get(dut, None) is None:
148 if tbs[dut].get(dver, None) is None:
149 tbs[dut][dver] = dict()
150 if tbs[dut][dver].get(infra, None) is None:
151 tbs[dut][dver][infra] = dict()
152 tbs[dut][dver][infra]["core"] = list()
153 tbs[dut][dver][infra]["fsize"] = list()
154 tbs[dut][dver][infra]["ttype"] = list()
155 if core.upper() not in tbs[dut][dver][infra]["core"]:
156 tbs[dut][dver][infra]["core"].append(core.upper())
157 if fsize.upper() not in tbs[dut][dver][infra]["fsize"]:
158 tbs[dut][dver][infra]["fsize"].append(fsize.upper())
159 if row["test_type"] == "mrr":
160 if "MRR" not in tbs[dut][dver][infra]["ttype"]:
161 tbs[dut][dver][infra]["ttype"].append("MRR")
162 elif row["test_type"] == "ndrpdr":
163 if "NDR" not in tbs[dut][dver][infra]["ttype"]:
164 tbs[dut][dver][infra]["ttype"].extend(
165 ("NDR", "PDR", "Latency")
167 elif row["test_type"] == "hoststack" and \
168 row["tg_type"] in ("iperf", "vpp"):
169 if "BPS" not in tbs[dut][dver][infra]["ttype"]:
170 tbs[dut][dver][infra]["ttype"].append("BPS")
171 elif row["test_type"] == "hoststack" and row["tg_type"] == "ab":
172 if "CPS" not in tbs[dut][dver][infra]["ttype"]:
173 tbs[dut][dver][infra]["ttype"].extend(("CPS", "RPS", ))
174 elif row["test_type"] == "soak":
175 if "SOAK" not in tbs[dut][dver][infra]["ttype"]:
176 tbs[dut][dver][infra]["ttype"].append("SOAK")
180 self._html_layout = str()
182 with open(self._html_layout_file, "r") as file_read:
183 self._html_layout = file_read.read()
184 except IOError as err:
186 f"Not possible to open the file {self._html_layout_file}\n{err}"
190 with open(self._graph_layout_file, "r") as file_read:
191 self._graph_layout = load(file_read, Loader=FullLoader)
192 except IOError as err:
194 f"Not possible to open the file {self._graph_layout_file}\n"
197 except YAMLError as err:
199 f"An error occurred while parsing the specification file "
200 f"{self._graph_layout_file}\n{err}"
204 with open(self._tooltip_file, "r") as file_read:
205 self._tooltips = load(file_read, Loader=FullLoader)
206 except IOError as err:
208 f"Not possible to open the file {self._tooltip_file}\n{err}"
210 except YAMLError as err:
212 f"An error occurred while parsing the specification file "
213 f"{self._tooltip_file}\n{err}"
217 if self._app is not None and hasattr(self, "callbacks"):
218 self.callbacks(self._app)
221 def html_layout(self):
222 return self._html_layout
224 def add_content(self):
225 """Top level method which generated the web page.
228 - Store for user input data,
230 - Main area with control panel and ploting area.
232 If no HTML layout is provided, an error message is displayed instead.
234 :returns: The HTML div with the whole page.
238 if self.html_layout and self._tbs:
246 children=[navbar_report((False, True, False, False)), ]
252 dcc.Store(id="store-control-panel"),
253 dcc.Store(id="store-selected"),
254 dcc.Store(id="store-table-data"),
255 dcc.Store(id="store-filtered-table-data"),
256 dcc.Location(id="url", refresh=False),
257 self._add_ctrl_col(),
258 self._add_plotting_col()
264 id="offcanvas-details",
265 title="Test Details",
270 delay_show=C.SPINNER_DELAY
275 id="offcanvas-metadata",
276 title="Detailed Information",
280 dbc.Row(id="metadata-tput-lat"),
281 dbc.Row(id="metadata-hdrh-graph")
284 delay_show=C.SPINNER_DELAY
288 id="offcanvas-documentation",
289 title="Documentation",
292 children=html.Iframe(
293 src=C.URL_DOC_REL_NOTES,
313 def _add_ctrl_col(self) -> dbc.Col:
314 """Add column with controls. It is placed on the left side.
316 :returns: Column with the control panel.
321 children=self._add_ctrl_panel(),
322 className="sticky-top"
326 def _add_plotting_col(self) -> dbc.Col:
327 """Add column with plots. It is placed on the right side.
329 :returns: Column with plots.
333 id="col-plotting-area",
339 class_name="g-0 p-0",
350 def _add_ctrl_panel(self) -> list:
351 """Add control panel.
353 :returns: Control panel.
359 class_name="g-0 p-1",
364 show_tooltip(self._tooltips, "help-dut", "DUT")
367 id={"type": "ctrl-dd", "index": "dut"},
368 placeholder="Select a Device under Test...",
371 {"label": k, "value": k} \
372 for k in self._tbs.keys()
374 key=lambda d: d["label"]
383 class_name="g-0 p-1",
387 dbc.InputGroupText(show_tooltip(
390 "CSIT and DUT Version"
393 id={"type": "ctrl-dd", "index": "dutver"},
394 placeholder="Select a CSIT and DUT Version...")
401 class_name="g-0 p-1",
405 dbc.InputGroupText(show_tooltip(
411 id={"type": "ctrl-dd", "index": "infra"},
413 "Select a Physical Test Bed Topology..."
421 class_name="g-0 p-1",
425 dbc.InputGroupText(show_tooltip(
431 id={"type": "ctrl-cl", "index": "frmsize"},
436 style={"align-items": "center"},
442 class_name="g-0 p-1",
446 dbc.InputGroupText(show_tooltip(
452 id={"type": "ctrl-cl", "index": "core"},
457 style={"align-items": "center"},
463 class_name="g-0 p-1",
467 dbc.InputGroupText(show_tooltip(
473 id={"type": "ctrl-cl", "index": "ttype"},
478 style={"align-items": "center"},
487 class_name="g-0 p-1",
491 dbc.InputGroupText(show_tooltip(
493 "help-cmp-parameter",
497 id={"type": "ctrl-dd", "index": "cmpprm"},
498 placeholder="Select a Parameter..."
506 class_name="g-0 p-1",
510 dbc.InputGroupText(show_tooltip(
516 id={"type": "ctrl-dd", "index": "cmpval"},
517 placeholder="Select a Value..."
528 class_name="g-0 p-1",
535 "value": "normalize",
536 "label": "Normalize to 2GHz CPU frequency"
546 "label": "Remove Extreme Outliers"
553 style={"align-items": "center"},
565 html.H5("Reference Value")
581 html.H5("Compared Value")
597 html.H5("Data Manipulations")
612 def _get_plotting_area(
617 """Generate the plotting area with all its content.
619 :param title: The title of the comparison table.
620 :param table: Comparison table to be displayed.
621 :param url: URL to be displayed in the modal window.
623 :type table: pandas.DataFrame
625 :returns: List of rows with elements to be displayed in the plotting
634 "No data for comparison.",
637 class_name="g-0 p-1",
643 for idx, col in enumerate(table.columns):
653 l_col = col.rsplit(" ", 2)
655 "name": [l_col[0], " ".join(l_col[-2:])],
660 "format": Format(precision=2, scheme=Scheme.fixed)
665 children=html.H5(title),
671 children=dash_table.DataTable(
672 id={"type": "table", "index": "comparison"},
674 data=table.to_dict("records"),
675 merge_duplicate_headers=True,
677 filter_action="custom",
679 sort_action="custom",
684 style_cell={"textAlign": "right"},
685 style_cell_conditional=[{
686 "if": {"column_id": "Test Name"},
705 "text-transform": "none",
706 "padding": "0rem 1rem"
711 dbc.ModalHeader(dbc.ModalTitle("URL")),
720 id="plot-btn-download",
721 children="Download Table",
725 "text-transform": "none",
726 "padding": "0rem 1rem"
729 dcc.Download(id="download-iterative-data"),
731 id="plot-btn-download-raw",
732 children="Download Raw Data",
736 "text-transform": "none",
737 "padding": "0rem 1rem"
740 dcc.Download(id="download-raw-data")
743 "d-grid gap-0 d-md-flex justify-content-md-end"
749 children=C.PLACEHOLDER,
754 def callbacks(self, app):
755 """Callbacks for the whole application.
757 :param app: The application.
763 Output("store-control-panel", "data"),
764 Output("store-selected", "data"),
765 Output("store-table-data", "data"),
766 Output("store-filtered-table-data", "data"),
767 Output("plotting-area", "children"),
768 Output({"type": "table", "index": ALL}, "data"),
769 Output({"type": "ctrl-dd", "index": "dut"}, "value"),
770 Output({"type": "ctrl-dd", "index": "dutver"}, "options"),
771 Output({"type": "ctrl-dd", "index": "dutver"}, "disabled"),
772 Output({"type": "ctrl-dd", "index": "dutver"}, "value"),
773 Output({"type": "ctrl-dd", "index": "infra"}, "options"),
774 Output({"type": "ctrl-dd", "index": "infra"}, "disabled"),
775 Output({"type": "ctrl-dd", "index": "infra"}, "value"),
776 Output({"type": "ctrl-cl", "index": "core"}, "options"),
777 Output({"type": "ctrl-cl", "index": "core"}, "value"),
778 Output({"type": "ctrl-cl", "index": "frmsize"}, "options"),
779 Output({"type": "ctrl-cl", "index": "frmsize"}, "value"),
780 Output({"type": "ctrl-cl", "index": "ttype"}, "options"),
781 Output({"type": "ctrl-cl", "index": "ttype"}, "value"),
782 Output({"type": "ctrl-dd", "index": "cmpprm"}, "options"),
783 Output({"type": "ctrl-dd", "index": "cmpprm"}, "disabled"),
784 Output({"type": "ctrl-dd", "index": "cmpprm"}, "value"),
785 Output({"type": "ctrl-dd", "index": "cmpval"}, "options"),
786 Output({"type": "ctrl-dd", "index": "cmpval"}, "disabled"),
787 Output({"type": "ctrl-dd", "index": "cmpval"}, "value"),
788 Output("normalize", "value"),
789 Output("outliers", "value")
792 State("store-control-panel", "data"),
793 State("store-selected", "data"),
794 State("store-table-data", "data"),
795 State("store-filtered-table-data", "data"),
796 State({"type": "table", "index": ALL}, "data")
799 Input("url", "href"),
800 Input("normalize", "value"),
801 Input("outliers", "value"),
802 Input({"type": "table", "index": ALL}, "filter_query"),
803 Input({"type": "table", "index": ALL}, "sort_by"),
804 Input({"type": "ctrl-dd", "index": ALL}, "value"),
805 Input({"type": "ctrl-cl", "index": ALL}, "value"),
806 Input({"type": "ctrl-btn", "index": ALL}, "n_clicks")
809 def _update_application(
812 store_table_data: list,
820 """Update the application when the event is detected.
823 ctrl_panel = ControlPanel(CP_PARAMS, control_panel)
836 parsed_url = url_decode(href)
838 url_params = parsed_url["params"]
843 plotting_area = no_update
845 trigger = Trigger(callback_context.triggered)
846 if trigger.type == "url" and url_params:
849 selected = literal_eval(url_params["selected"][0])
850 r_sel = selected["reference"]["selection"]
851 c_sel = selected["compare"]
852 normalize = literal_eval(url_params["norm"][0])
853 try: # Necessary for backward compatibility
854 outliers = literal_eval(url_params["outliers"][0])
855 except (KeyError, IndexError, AttributeError):
858 (selected["reference"]["set"] == True) and
859 (c_sel["set"] == True)
861 except (KeyError, IndexError, AttributeError):
865 "dut-val": r_sel["dut"],
866 "dutver-opt": generate_options(
867 self._tbs[r_sel["dut"]].keys()
870 "dutver-val": r_sel["dutver"],
871 "infra-opt": generate_options(
872 self._tbs[r_sel["dut"]][r_sel["dutver"]].keys()
875 "infra-val": r_sel["infra"],
876 "core-opt": generate_options(
877 self._tbs[r_sel["dut"]][r_sel["dutver"]]\
878 [r_sel["infra"]]["core"]
880 "core-val": r_sel["core"],
881 "frmsize-opt": generate_options(
882 self._tbs[r_sel["dut"]][r_sel["dutver"]]\
883 [r_sel["infra"]]["fsize"]
885 "frmsize-val": r_sel["frmsize"],
886 "ttype-opt": generate_options(
887 self._tbs[r_sel["dut"]][r_sel["dutver"]]\
888 [r_sel["infra"]]["ttype"]
890 "ttype-val": r_sel["ttype"],
891 "normalize-val": normalize,
892 "outliers-val": outliers
895 for itm, label in CMP_PARAMS.items():
896 if len(ctrl_panel.get(f"{itm}-opt")) > 1:
897 opts.append({"label": label, "value": itm})
900 "cmp-par-dis": False,
901 "cmp-par-val": c_sel["parameter"]
904 for itm in ctrl_panel.get(f"{c_sel['parameter']}-opt"):
905 set_val = ctrl_panel.get(f"{c_sel['parameter']}-val")
906 if isinstance(set_val, list):
907 if itm["value"] not in set_val:
910 if itm["value"] != set_val:
914 "cmp-val-dis": False,
915 "cmp-val-val": c_sel["value"]
918 elif trigger.type == "normalize":
919 ctrl_panel.set({"normalize-val": normalize})
921 elif trigger.type == "outliers":
922 ctrl_panel.set({"outliers-val": outliers})
924 elif trigger.type == "ctrl-dd":
925 if trigger.idx == "dut":
927 opts = generate_options(self._tbs[trigger.value].keys())
933 "dut-val": trigger.value,
935 "dutver-dis": disabled,
942 "frmsize-opt": list(),
943 "frmsize-val": list(),
946 "cmp-par-opt": list(),
948 "cmp-par-val": str(),
949 "cmp-val-opt": list(),
953 elif trigger.idx == "dutver":
955 dut = ctrl_panel.get("dut-val")
956 dver = self._tbs[dut][trigger.value]
957 opts = generate_options(dver.keys())
963 "dutver-val": trigger.value,
965 "infra-dis": disabled,
969 "frmsize-opt": list(),
970 "frmsize-val": list(),
973 "cmp-par-opt": list(),
975 "cmp-par-val": str(),
976 "cmp-val-opt": list(),
980 elif trigger.idx == "infra":
981 dut = ctrl_panel.get("dut-val")
982 dver = ctrl_panel.get("dutver-val")
983 if all((dut, dver, trigger.value, )):
984 driver = self._tbs[dut][dver][trigger.value]
986 "infra-val": trigger.value,
987 "core-opt": generate_options(driver["core"]),
989 "frmsize-opt": generate_options(driver["fsize"]),
990 "frmsize-val": list(),
991 "ttype-opt": generate_options(driver["ttype"]),
993 "cmp-par-opt": list(),
995 "cmp-par-val": str(),
996 "cmp-val-opt": list(),
1000 elif trigger.idx == "cmpprm":
1001 value = trigger.value
1003 for itm in ctrl_panel.get(f"{value}-opt"):
1004 set_val = ctrl_panel.get(f"{value}-val")
1005 if isinstance(set_val, list):
1006 if itm["value"] == "Latency":
1008 if itm["value"] not in set_val:
1011 if itm["value"] != set_val:
1014 "cmp-par-val": value,
1015 "cmp-val-opt": opts,
1016 "cmp-val-dis": False,
1017 "cmp-val-val": str()
1019 elif trigger.idx == "cmpval":
1020 ctrl_panel.set({"cmp-val-val": trigger.value})
1021 selected["reference"] = {
1024 "dut": ctrl_panel.get("dut-val"),
1025 "dutver": ctrl_panel.get("dutver-val"),
1026 "infra": ctrl_panel.get("infra-val"),
1027 "core": ctrl_panel.get("core-val"),
1028 "frmsize": ctrl_panel.get("frmsize-val"),
1029 "ttype": ctrl_panel.get("ttype-val")
1032 selected["compare"] = {
1034 "parameter": ctrl_panel.get("cmp-par-val"),
1035 "value": trigger.value
1038 elif trigger.type == "ctrl-cl":
1039 ctrl_panel.set({f"{trigger.idx}-val": trigger.value})
1040 if all((ctrl_panel.get("core-val"),
1041 ctrl_panel.get("frmsize-val"),
1042 ctrl_panel.get("ttype-val"), )):
1043 if "Latency" in ctrl_panel.get("ttype-val"):
1044 ctrl_panel.set({"ttype-val": ["Latency", ]})
1046 for itm, label in CMP_PARAMS.items():
1047 if "Latency" in ctrl_panel.get("ttype-val") and \
1050 if len(ctrl_panel.get(f"{itm}-opt")) > 1:
1051 if isinstance(ctrl_panel.get(f"{itm}-val"), list):
1052 if len(ctrl_panel.get(f"{itm}-opt")) == \
1053 len(ctrl_panel.get(f"{itm}-val")):
1055 opts.append({"label": label, "value": itm})
1057 "cmp-par-opt": opts,
1058 "cmp-par-dis": False,
1059 "cmp-par-val": str(),
1060 "cmp-val-opt": list(),
1061 "cmp-val-dis": True,
1062 "cmp-val-val": str()
1066 "cmp-par-opt": list(),
1067 "cmp-par-dis": True,
1068 "cmp-par-val": str(),
1069 "cmp-val-opt": list(),
1070 "cmp-val-dis": True,
1071 "cmp-val-val": str()
1073 elif trigger.type == "table" and trigger.idx == "comparison":
1074 if trigger.parameter == "filter_query":
1075 filtered_data = filter_table_data(
1079 elif trigger.parameter == "sort_by":
1080 filtered_data = sort_table_data(
1084 table_data = [filtered_data, ]
1086 if all((on_draw, selected["reference"]["set"],
1087 selected["compare"]["set"], )):
1088 title, table = comparison_table(
1091 normalize=normalize,
1093 remove_outliers=outliers
1095 plotting_area = self._get_plotting_area(
1101 "selected": selected,
1103 "outliers": outliers
1107 store_table_data = table.to_dict("records")
1108 filtered_data = store_table_data
1110 table_data = [store_table_data, ]
1120 ret_val.extend(ctrl_panel.values)
1124 Output("plot-mod-url", "is_open"),
1125 Input("plot-btn-url", "n_clicks"),
1126 State("plot-mod-url", "is_open")
1128 def toggle_plot_mod_url(n, is_open):
1129 """Toggle the modal window with url.
1136 Output("download-iterative-data", "data"),
1137 State("store-table-data", "data"),
1138 State("store-filtered-table-data", "data"),
1139 Input("plot-btn-download", "n_clicks"),
1140 prevent_initial_call=True
1142 def _download_comparison_data(
1144 filtered_table_data: list,
1147 """Download the data.
1149 :param table_data: Original unfiltered table data.
1150 :param filtered_table_data: Filtered table data.
1151 :type table_data: list
1152 :type filtered_table_data: list
1153 :returns: dict of data frame content (base64 encoded) and meta data
1154 used by the Download component.
1161 if filtered_table_data:
1162 table = pd.DataFrame.from_records(filtered_table_data)
1164 table = pd.DataFrame.from_records(table_data)
1166 return dcc.send_data_frame(table.to_csv, C.COMP_DOWNLOAD_FILE_NAME)
1169 Output("download-raw-data", "data"),
1170 State("store-selected", "data"),
1171 Input("plot-btn-download-raw", "n_clicks"),
1172 prevent_initial_call=True
1174 def _download_raw_comparison_data(selected: dict, _: int) -> dict:
1175 """Download the data.
1177 :param selected: Selected tests.
1178 :type selected: dict
1179 :returns: dict of data frame content (base64 encoded) and meta data
1180 used by the Download component.
1187 _, table = comparison_table(
1191 remove_outliers=False,
1195 return dcc.send_data_frame(
1196 table.dropna(how="all", axis=1).to_csv,
1197 f"raw_{C.COMP_DOWNLOAD_FILE_NAME}"
1201 Output("offcanvas-documentation", "is_open"),
1202 Input("btn-documentation", "n_clicks"),
1203 State("offcanvas-documentation", "is_open")
1205 def toggle_offcanvas_documentation(n_clicks, is_open):
1211 Output("offcanvas-details", "is_open"),
1212 Output("offcanvas-details", "children"),
1213 State("store-selected", "data"),
1214 State("store-filtered-table-data", "data"),
1215 State("normalize", "value"),
1216 State("outliers", "value"),
1217 Input({"type": "table", "index": ALL}, "active_cell"),
1218 prevent_initial_call=True
1220 def show_test_data(cp_sel, table, normalize, outliers, *_):
1221 """Show offcanvas with graphs and tables based on selected test(s).
1224 trigger = Trigger(callback_context.triggered)
1225 if not all((trigger.value, cp_sel["reference"]["set"], \
1226 cp_sel["compare"]["set"])):
1230 test_name = pd.DataFrame.from_records(table).\
1231 iloc[[trigger.value["row"]]]["Test Name"].iloc[0]
1232 dut = cp_sel["reference"]["selection"]["dut"]
1233 rls, dutver = cp_sel["reference"]["selection"]["dutver"].\
1235 phy = cp_sel["reference"]["selection"]["infra"]
1236 framesize, core, test_id = test_name.split("-", 2)
1237 test, ttype = test_id.rsplit("-", 1)
1238 ttype = "pdr" if ttype == "latency" else ttype
1239 l_phy = phy.split("-")
1240 tb = "-".join(l_phy[:2])
1242 stype = "ndrpdr" if ttype in ("ndr", "pdr") else ttype
1243 except(KeyError, IndexError, AttributeError, ValueError):
1246 df = pd.DataFrame(self._data.loc[(
1247 (self._data["dut_type"] == dut) &
1248 (self._data["dut_version"] == dutver) &
1249 (self._data["release"] == rls)
1251 df = df[df.job.str.endswith(tb)]
1252 df = df[df.test_id.str.contains(
1253 f"{nic}.*{test}-{stype}", regex=True
1258 l_test_id = df["test_id"].iloc[0].split(".")
1259 area = ".".join(l_test_id[3:-2])
1262 "id": f"{test}-{ttype}",
1269 "framesize": framesize,
1274 c_sel = deepcopy(r_sel)
1275 param = cp_sel["compare"]["parameter"]
1276 val = cp_sel["compare"]["value"].lower()
1277 if param == "dutver":
1278 c_sel["rls"], c_sel["dutver"] = val.split("-", 1)
1279 elif param == "ttype":
1280 c_sel["id"] = f"{test}-{val}"
1281 c_sel["testtype"] = val
1282 elif param == "infra":
1287 r_sel["id"] = "-".join(
1288 (r_sel["phy"], r_sel["framesize"], r_sel["core"], r_sel["id"])
1290 c_sel["id"] = "-".join(
1291 (c_sel["phy"], c_sel["framesize"], c_sel["core"], c_sel["id"])
1293 selected = [r_sel, c_sel]
1295 indexes = ("tput", "bandwidth", "lat")
1296 graphs = graph_iterative(
1304 for graph, idx in zip(graphs, indexes):
1306 cols.append(dbc.Col(dcc.Graph(
1308 id={"type": "graph-iter", "index": idx},
1314 class_name="g-0 p-0",
1315 children=dbc.Alert(test, color="info"),
1317 dbc.Row(class_name="g-0 p-0", children=cols)
1320 return True, ret_val
1323 Output("metadata-tput-lat", "children"),
1324 Output("metadata-hdrh-graph", "children"),
1325 Output("offcanvas-metadata", "is_open"),
1326 Input({"type": "graph-iter", "index": ALL}, "clickData"),
1327 prevent_initial_call=True
1329 def _show_metadata_from_graph(iter_data: dict) -> tuple:
1330 """Generates the data for the offcanvas displayed when a particular
1331 point in a graph is clicked on.
1334 trigger = Trigger(callback_context.triggered)
1335 if not trigger.value:
1338 if trigger.type == "graph-iter":
1339 return show_iterative_graph_data(
1340 trigger, iter_data, self._graph_layout)