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", ))
177 self._html_layout = str()
179 with open(self._html_layout_file, "r") as file_read:
180 self._html_layout = file_read.read()
181 except IOError as err:
183 f"Not possible to open the file {self._html_layout_file}\n{err}"
187 with open(self._graph_layout_file, "r") as file_read:
188 self._graph_layout = load(file_read, Loader=FullLoader)
189 except IOError as err:
191 f"Not possible to open the file {self._graph_layout_file}\n"
194 except YAMLError as err:
196 f"An error occurred while parsing the specification file "
197 f"{self._graph_layout_file}\n{err}"
201 with open(self._tooltip_file, "r") as file_read:
202 self._tooltips = load(file_read, Loader=FullLoader)
203 except IOError as err:
205 f"Not possible to open the file {self._tooltip_file}\n{err}"
207 except YAMLError as err:
209 f"An error occurred while parsing the specification file "
210 f"{self._tooltip_file}\n{err}"
214 if self._app is not None and hasattr(self, "callbacks"):
215 self.callbacks(self._app)
218 def html_layout(self):
219 return self._html_layout
221 def add_content(self):
222 """Top level method which generated the web page.
225 - Store for user input data,
227 - Main area with control panel and ploting area.
229 If no HTML layout is provided, an error message is displayed instead.
231 :returns: The HTML div with the whole page.
235 if self.html_layout and self._tbs:
243 children=[navbar_report((False, True, False, False)), ]
249 dcc.Store(id="store-control-panel"),
250 dcc.Store(id="store-selected"),
251 dcc.Store(id="store-table-data"),
252 dcc.Store(id="store-filtered-table-data"),
253 dcc.Location(id="url", refresh=False),
254 self._add_ctrl_col(),
255 self._add_plotting_col()
261 id="offcanvas-details",
262 title="Test Details",
267 delay_show=C.SPINNER_DELAY
272 id="offcanvas-metadata",
273 title="Detailed Information",
277 dbc.Row(id="metadata-tput-lat"),
278 dbc.Row(id="metadata-hdrh-graph")
281 delay_show=C.SPINNER_DELAY
285 id="offcanvas-documentation",
286 title="Documentation",
289 children=html.Iframe(
290 src=C.URL_DOC_REL_NOTES,
310 def _add_ctrl_col(self) -> dbc.Col:
311 """Add column with controls. It is placed on the left side.
313 :returns: Column with the control panel.
318 children=self._add_ctrl_panel(),
319 className="sticky-top"
323 def _add_plotting_col(self) -> dbc.Col:
324 """Add column with plots. It is placed on the right side.
326 :returns: Column with plots.
330 id="col-plotting-area",
336 class_name="g-0 p-0",
347 def _add_ctrl_panel(self) -> list:
348 """Add control panel.
350 :returns: Control panel.
356 class_name="g-0 p-1",
361 show_tooltip(self._tooltips, "help-dut", "DUT")
364 id={"type": "ctrl-dd", "index": "dut"},
365 placeholder="Select a Device under Test...",
368 {"label": k, "value": k} \
369 for k in self._tbs.keys()
371 key=lambda d: d["label"]
380 class_name="g-0 p-1",
384 dbc.InputGroupText(show_tooltip(
387 "CSIT and DUT Version"
390 id={"type": "ctrl-dd", "index": "dutver"},
391 placeholder="Select a CSIT and DUT Version...")
398 class_name="g-0 p-1",
402 dbc.InputGroupText(show_tooltip(
408 id={"type": "ctrl-dd", "index": "infra"},
410 "Select a Physical Test Bed Topology..."
418 class_name="g-0 p-1",
422 dbc.InputGroupText(show_tooltip(
428 id={"type": "ctrl-cl", "index": "frmsize"},
433 style={"align-items": "center"},
439 class_name="g-0 p-1",
443 dbc.InputGroupText(show_tooltip(
449 id={"type": "ctrl-cl", "index": "core"},
454 style={"align-items": "center"},
460 class_name="g-0 p-1",
464 dbc.InputGroupText(show_tooltip(
470 id={"type": "ctrl-cl", "index": "ttype"},
475 style={"align-items": "center"},
484 class_name="g-0 p-1",
488 dbc.InputGroupText(show_tooltip(
490 "help-cmp-parameter",
494 id={"type": "ctrl-dd", "index": "cmpprm"},
495 placeholder="Select a Parameter..."
503 class_name="g-0 p-1",
507 dbc.InputGroupText(show_tooltip(
513 id={"type": "ctrl-dd", "index": "cmpval"},
514 placeholder="Select a Value..."
525 class_name="g-0 p-1",
532 "value": "normalize",
533 "label": "Normalize to 2GHz CPU frequency"
543 "label": "Remove Extreme Outliers"
550 style={"align-items": "center"},
562 html.H5("Reference Value")
578 html.H5("Compared Value")
594 html.H5("Data Manipulations")
609 def _get_plotting_area(
614 """Generate the plotting area with all its content.
616 :param title: The title of the comparison table.
617 :param table: Comparison table to be displayed.
618 :param url: URL to be displayed in the modal window.
620 :type table: pandas.DataFrame
622 :returns: List of rows with elements to be displayed in the plotting
631 "No data for comparison.",
634 class_name="g-0 p-1",
640 for idx, col in enumerate(table.columns):
650 l_col = col.rsplit(" ", 2)
652 "name": [l_col[0], " ".join(l_col[-2:])],
657 "format": Format(precision=2, scheme=Scheme.fixed)
662 children=html.H5(title),
668 children=dash_table.DataTable(
669 id={"type": "table", "index": "comparison"},
671 data=table.to_dict("records"),
672 merge_duplicate_headers=True,
674 filter_action="custom",
676 sort_action="custom",
681 style_cell={"textAlign": "right"},
682 style_cell_conditional=[{
683 "if": {"column_id": "Test Name"},
702 "text-transform": "none",
703 "padding": "0rem 1rem"
708 dbc.ModalHeader(dbc.ModalTitle("URL")),
717 id="plot-btn-download",
718 children="Download Table",
722 "text-transform": "none",
723 "padding": "0rem 1rem"
726 dcc.Download(id="download-iterative-data"),
728 id="plot-btn-download-raw",
729 children="Download Raw Data",
733 "text-transform": "none",
734 "padding": "0rem 1rem"
737 dcc.Download(id="download-raw-data")
740 "d-grid gap-0 d-md-flex justify-content-md-end"
746 children=C.PLACEHOLDER,
751 def callbacks(self, app):
752 """Callbacks for the whole application.
754 :param app: The application.
760 Output("store-control-panel", "data"),
761 Output("store-selected", "data"),
762 Output("store-table-data", "data"),
763 Output("store-filtered-table-data", "data"),
764 Output("plotting-area", "children"),
765 Output({"type": "table", "index": ALL}, "data"),
766 Output({"type": "ctrl-dd", "index": "dut"}, "value"),
767 Output({"type": "ctrl-dd", "index": "dutver"}, "options"),
768 Output({"type": "ctrl-dd", "index": "dutver"}, "disabled"),
769 Output({"type": "ctrl-dd", "index": "dutver"}, "value"),
770 Output({"type": "ctrl-dd", "index": "infra"}, "options"),
771 Output({"type": "ctrl-dd", "index": "infra"}, "disabled"),
772 Output({"type": "ctrl-dd", "index": "infra"}, "value"),
773 Output({"type": "ctrl-cl", "index": "core"}, "options"),
774 Output({"type": "ctrl-cl", "index": "core"}, "value"),
775 Output({"type": "ctrl-cl", "index": "frmsize"}, "options"),
776 Output({"type": "ctrl-cl", "index": "frmsize"}, "value"),
777 Output({"type": "ctrl-cl", "index": "ttype"}, "options"),
778 Output({"type": "ctrl-cl", "index": "ttype"}, "value"),
779 Output({"type": "ctrl-dd", "index": "cmpprm"}, "options"),
780 Output({"type": "ctrl-dd", "index": "cmpprm"}, "disabled"),
781 Output({"type": "ctrl-dd", "index": "cmpprm"}, "value"),
782 Output({"type": "ctrl-dd", "index": "cmpval"}, "options"),
783 Output({"type": "ctrl-dd", "index": "cmpval"}, "disabled"),
784 Output({"type": "ctrl-dd", "index": "cmpval"}, "value"),
785 Output("normalize", "value"),
786 Output("outliers", "value")
789 State("store-control-panel", "data"),
790 State("store-selected", "data"),
791 State("store-table-data", "data"),
792 State("store-filtered-table-data", "data"),
793 State({"type": "table", "index": ALL}, "data")
796 Input("url", "href"),
797 Input("normalize", "value"),
798 Input("outliers", "value"),
799 Input({"type": "table", "index": ALL}, "filter_query"),
800 Input({"type": "table", "index": ALL}, "sort_by"),
801 Input({"type": "ctrl-dd", "index": ALL}, "value"),
802 Input({"type": "ctrl-cl", "index": ALL}, "value"),
803 Input({"type": "ctrl-btn", "index": ALL}, "n_clicks")
806 def _update_application(
809 store_table_data: list,
817 """Update the application when the event is detected.
820 ctrl_panel = ControlPanel(CP_PARAMS, control_panel)
833 parsed_url = url_decode(href)
835 url_params = parsed_url["params"]
840 plotting_area = no_update
842 trigger = Trigger(callback_context.triggered)
843 if trigger.type == "url" and url_params:
846 selected = literal_eval(url_params["selected"][0])
847 r_sel = selected["reference"]["selection"]
848 c_sel = selected["compare"]
849 normalize = literal_eval(url_params["norm"][0])
850 try: # Necessary for backward compatibility
851 outliers = literal_eval(url_params["outliers"][0])
852 except (KeyError, IndexError, AttributeError):
855 (selected["reference"]["set"] == True) and
856 (c_sel["set"] == True)
858 except (KeyError, IndexError, AttributeError):
862 "dut-val": r_sel["dut"],
863 "dutver-opt": generate_options(
864 self._tbs[r_sel["dut"]].keys()
867 "dutver-val": r_sel["dutver"],
868 "infra-opt": generate_options(
869 self._tbs[r_sel["dut"]][r_sel["dutver"]].keys()
872 "infra-val": r_sel["infra"],
873 "core-opt": generate_options(
874 self._tbs[r_sel["dut"]][r_sel["dutver"]]\
875 [r_sel["infra"]]["core"]
877 "core-val": r_sel["core"],
878 "frmsize-opt": generate_options(
879 self._tbs[r_sel["dut"]][r_sel["dutver"]]\
880 [r_sel["infra"]]["fsize"]
882 "frmsize-val": r_sel["frmsize"],
883 "ttype-opt": generate_options(
884 self._tbs[r_sel["dut"]][r_sel["dutver"]]\
885 [r_sel["infra"]]["ttype"]
887 "ttype-val": r_sel["ttype"],
888 "normalize-val": normalize,
889 "outliers-val": outliers
892 for itm, label in CMP_PARAMS.items():
893 if len(ctrl_panel.get(f"{itm}-opt")) > 1:
894 opts.append({"label": label, "value": itm})
897 "cmp-par-dis": False,
898 "cmp-par-val": c_sel["parameter"]
901 for itm in ctrl_panel.get(f"{c_sel['parameter']}-opt"):
902 set_val = ctrl_panel.get(f"{c_sel['parameter']}-val")
903 if isinstance(set_val, list):
904 if itm["value"] not in set_val:
907 if itm["value"] != set_val:
911 "cmp-val-dis": False,
912 "cmp-val-val": c_sel["value"]
915 elif trigger.type == "normalize":
916 ctrl_panel.set({"normalize-val": normalize})
918 elif trigger.type == "outliers":
919 ctrl_panel.set({"outliers-val": outliers})
921 elif trigger.type == "ctrl-dd":
922 if trigger.idx == "dut":
924 opts = generate_options(self._tbs[trigger.value].keys())
930 "dut-val": trigger.value,
932 "dutver-dis": disabled,
939 "frmsize-opt": list(),
940 "frmsize-val": list(),
943 "cmp-par-opt": list(),
945 "cmp-par-val": str(),
946 "cmp-val-opt": list(),
950 elif trigger.idx == "dutver":
952 dut = ctrl_panel.get("dut-val")
953 dver = self._tbs[dut][trigger.value]
954 opts = generate_options(dver.keys())
960 "dutver-val": trigger.value,
962 "infra-dis": disabled,
966 "frmsize-opt": list(),
967 "frmsize-val": list(),
970 "cmp-par-opt": list(),
972 "cmp-par-val": str(),
973 "cmp-val-opt": list(),
977 elif trigger.idx == "infra":
978 dut = ctrl_panel.get("dut-val")
979 dver = ctrl_panel.get("dutver-val")
980 if all((dut, dver, trigger.value, )):
981 driver = self._tbs[dut][dver][trigger.value]
983 "infra-val": trigger.value,
984 "core-opt": generate_options(driver["core"]),
986 "frmsize-opt": generate_options(driver["fsize"]),
987 "frmsize-val": list(),
988 "ttype-opt": generate_options(driver["ttype"]),
990 "cmp-par-opt": list(),
992 "cmp-par-val": str(),
993 "cmp-val-opt": list(),
997 elif trigger.idx == "cmpprm":
998 value = trigger.value
1000 for itm in ctrl_panel.get(f"{value}-opt"):
1001 set_val = ctrl_panel.get(f"{value}-val")
1002 if isinstance(set_val, list):
1003 if itm["value"] == "Latency":
1005 if itm["value"] not in set_val:
1008 if itm["value"] != set_val:
1011 "cmp-par-val": value,
1012 "cmp-val-opt": opts,
1013 "cmp-val-dis": False,
1014 "cmp-val-val": str()
1016 elif trigger.idx == "cmpval":
1017 ctrl_panel.set({"cmp-val-val": trigger.value})
1018 selected["reference"] = {
1021 "dut": ctrl_panel.get("dut-val"),
1022 "dutver": ctrl_panel.get("dutver-val"),
1023 "infra": ctrl_panel.get("infra-val"),
1024 "core": ctrl_panel.get("core-val"),
1025 "frmsize": ctrl_panel.get("frmsize-val"),
1026 "ttype": ctrl_panel.get("ttype-val")
1029 selected["compare"] = {
1031 "parameter": ctrl_panel.get("cmp-par-val"),
1032 "value": trigger.value
1035 elif trigger.type == "ctrl-cl":
1036 ctrl_panel.set({f"{trigger.idx}-val": trigger.value})
1037 if all((ctrl_panel.get("core-val"),
1038 ctrl_panel.get("frmsize-val"),
1039 ctrl_panel.get("ttype-val"), )):
1040 if "Latency" in ctrl_panel.get("ttype-val"):
1041 ctrl_panel.set({"ttype-val": ["Latency", ]})
1043 for itm, label in CMP_PARAMS.items():
1044 if "Latency" in ctrl_panel.get("ttype-val") and \
1047 if len(ctrl_panel.get(f"{itm}-opt")) > 1:
1048 if isinstance(ctrl_panel.get(f"{itm}-val"), list):
1049 if len(ctrl_panel.get(f"{itm}-opt")) == \
1050 len(ctrl_panel.get(f"{itm}-val")):
1052 opts.append({"label": label, "value": itm})
1054 "cmp-par-opt": opts,
1055 "cmp-par-dis": False,
1056 "cmp-par-val": str(),
1057 "cmp-val-opt": list(),
1058 "cmp-val-dis": True,
1059 "cmp-val-val": str()
1063 "cmp-par-opt": list(),
1064 "cmp-par-dis": True,
1065 "cmp-par-val": str(),
1066 "cmp-val-opt": list(),
1067 "cmp-val-dis": True,
1068 "cmp-val-val": str()
1070 elif trigger.type == "table" and trigger.idx == "comparison":
1071 if trigger.parameter == "filter_query":
1072 filtered_data = filter_table_data(
1076 elif trigger.parameter == "sort_by":
1077 filtered_data = sort_table_data(
1081 table_data = [filtered_data, ]
1083 if all((on_draw, selected["reference"]["set"],
1084 selected["compare"]["set"], )):
1085 title, table = comparison_table(
1088 normalize=normalize,
1090 remove_outliers=outliers
1092 plotting_area = self._get_plotting_area(
1098 "selected": selected,
1100 "outliers": outliers
1104 store_table_data = table.to_dict("records")
1105 filtered_data = store_table_data
1107 table_data = [store_table_data, ]
1117 ret_val.extend(ctrl_panel.values)
1121 Output("plot-mod-url", "is_open"),
1122 Input("plot-btn-url", "n_clicks"),
1123 State("plot-mod-url", "is_open")
1125 def toggle_plot_mod_url(n, is_open):
1126 """Toggle the modal window with url.
1133 Output("download-iterative-data", "data"),
1134 State("store-table-data", "data"),
1135 State("store-filtered-table-data", "data"),
1136 Input("plot-btn-download", "n_clicks"),
1137 prevent_initial_call=True
1139 def _download_comparison_data(
1141 filtered_table_data: list,
1144 """Download the data.
1146 :param table_data: Original unfiltered table data.
1147 :param filtered_table_data: Filtered table data.
1148 :type table_data: list
1149 :type filtered_table_data: list
1150 :returns: dict of data frame content (base64 encoded) and meta data
1151 used by the Download component.
1158 if filtered_table_data:
1159 table = pd.DataFrame.from_records(filtered_table_data)
1161 table = pd.DataFrame.from_records(table_data)
1163 return dcc.send_data_frame(table.to_csv, C.COMP_DOWNLOAD_FILE_NAME)
1166 Output("download-raw-data", "data"),
1167 State("store-selected", "data"),
1168 Input("plot-btn-download-raw", "n_clicks"),
1169 prevent_initial_call=True
1171 def _download_raw_comparison_data(selected: dict, _: int) -> dict:
1172 """Download the data.
1174 :param selected: Selected tests.
1175 :type selected: dict
1176 :returns: dict of data frame content (base64 encoded) and meta data
1177 used by the Download component.
1184 _, table = comparison_table(
1188 remove_outliers=False,
1192 return dcc.send_data_frame(
1193 table.dropna(how="all", axis=1).to_csv,
1194 f"raw_{C.COMP_DOWNLOAD_FILE_NAME}"
1198 Output("offcanvas-documentation", "is_open"),
1199 Input("btn-documentation", "n_clicks"),
1200 State("offcanvas-documentation", "is_open")
1202 def toggle_offcanvas_documentation(n_clicks, is_open):
1208 Output("offcanvas-details", "is_open"),
1209 Output("offcanvas-details", "children"),
1210 State("store-selected", "data"),
1211 State("store-filtered-table-data", "data"),
1212 State("normalize", "value"),
1213 State("outliers", "value"),
1214 Input({"type": "table", "index": ALL}, "active_cell"),
1215 prevent_initial_call=True
1217 def show_test_data(cp_sel, table, normalize, outliers, *_):
1218 """Show offcanvas with graphs and tables based on selected test(s).
1221 trigger = Trigger(callback_context.triggered)
1222 if not all((trigger.value, cp_sel["reference"]["set"], \
1223 cp_sel["compare"]["set"])):
1227 test_name = pd.DataFrame.from_records(table).\
1228 iloc[[trigger.value["row"]]]["Test Name"].iloc[0]
1229 dut = cp_sel["reference"]["selection"]["dut"]
1230 rls, dutver = cp_sel["reference"]["selection"]["dutver"].\
1232 phy = cp_sel["reference"]["selection"]["infra"]
1233 framesize, core, test_id = test_name.split("-", 2)
1234 test, ttype = test_id.rsplit("-", 1)
1235 ttype = "pdr" if ttype == "latency" else ttype
1236 l_phy = phy.split("-")
1237 tb = "-".join(l_phy[:2])
1239 stype = "ndrpdr" if ttype in ("ndr", "pdr") else ttype
1240 except(KeyError, IndexError, AttributeError, ValueError):
1243 df = pd.DataFrame(self._data.loc[(
1244 (self._data["dut_type"] == dut) &
1245 (self._data["dut_version"] == dutver) &
1246 (self._data["release"] == rls)
1248 df = df[df.job.str.endswith(tb)]
1249 df = df[df.test_id.str.contains(
1250 f"{nic}.*{test}-{stype}", regex=True
1255 l_test_id = df["test_id"].iloc[0].split(".")
1256 area = ".".join(l_test_id[3:-2])
1259 "id": f"{test}-{ttype}",
1266 "framesize": framesize,
1271 c_sel = deepcopy(r_sel)
1272 param = cp_sel["compare"]["parameter"]
1273 val = cp_sel["compare"]["value"].lower()
1274 if param == "dutver":
1275 c_sel["rls"], c_sel["dutver"] = val.split("-", 1)
1276 elif param == "ttype":
1277 c_sel["id"] = f"{test}-{val}"
1278 c_sel["testtype"] = val
1279 elif param == "infra":
1284 r_sel["id"] = "-".join(
1285 (r_sel["phy"], r_sel["framesize"], r_sel["core"], r_sel["id"])
1287 c_sel["id"] = "-".join(
1288 (c_sel["phy"], c_sel["framesize"], c_sel["core"], c_sel["id"])
1290 selected = [r_sel, c_sel]
1292 indexes = ("tput", "bandwidth", "lat")
1293 graphs = graph_iterative(
1301 for graph, idx in zip(graphs, indexes):
1303 cols.append(dbc.Col(dcc.Graph(
1305 id={"type": "graph-iter", "index": idx},
1311 class_name="g-0 p-0",
1312 children=dbc.Alert(test, color="info"),
1314 dbc.Row(class_name="g-0 p-0", children=cols)
1317 return True, ret_val
1320 Output("metadata-tput-lat", "children"),
1321 Output("metadata-hdrh-graph", "children"),
1322 Output("offcanvas-metadata", "is_open"),
1323 Input({"type": "graph-iter", "index": ALL}, "clickData"),
1324 prevent_initial_call=True
1326 def _show_metadata_from_graph(iter_data: dict) -> tuple:
1327 """Generates the data for the offcanvas displayed when a particular
1328 point in a graph is clicked on.
1331 trigger = Trigger(callback_context.triggered)
1332 if not trigger.value:
1335 if trigger.type == "graph-iter":
1336 return show_iterative_graph_data(
1337 trigger, iter_data, self._graph_layout)