0cf1a9a942e21e15d2a03f160d51fedfc64b3723
[csit.git] / resources / tools / dash / app / pal / trending / layout.py
1 # Copyright (c) 2022 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:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
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.
13
14 """Plotly Dash HTML layout override.
15 """
16
17 import logging
18
19 import pandas as pd
20
21 from dash import dcc
22 from dash import html
23 from dash import callback_context, no_update
24 from dash import Input, Output, State
25 from dash.exceptions import PreventUpdate
26 import dash_bootstrap_components as dbc
27 from yaml import load, FullLoader, YAMLError
28 from datetime import datetime, timedelta
29
30 from ..data.data import Data
31 from .graphs import graph_trending, graph_hdrh_latency, \
32     select_trending_data
33
34
35 class Layout:
36     """
37     """
38
39     STYLE_HIDEN = {"display": "none"}
40     STYLE_BLOCK = {"display": "block", "vertical-align": "top"}
41     STYLE_INLINE ={
42         "display": "inline-block",
43         "vertical-align": "top"
44     }
45     NO_GRAPH = {"data": [], "layout": {}, "frames": []}
46
47     def __init__(self, app, html_layout_file, spec_file, graph_layout_file,
48         data_spec_file):
49         """
50         """
51
52         # Inputs
53         self._app = app
54         self._html_layout_file = html_layout_file
55         self._spec_file = spec_file
56         self._graph_layout_file = graph_layout_file
57         self._data_spec_file = data_spec_file
58
59         # Read the data:
60         data_mrr = Data(
61             data_spec_file=self._data_spec_file,
62             debug=True
63         ).read_trending_mrr(days=5)
64
65         data_ndrpdr = Data(
66             data_spec_file=self._data_spec_file,
67             debug=True
68         ).read_trending_ndrpdr(days=14)
69
70         self._data = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
71
72         # Read from files:
73         self._html_layout = ""
74         self._spec_tbs = None
75         self._graph_layout = None
76
77         try:
78             with open(self._html_layout_file, "r") as file_read:
79                 self._html_layout = file_read.read()
80         except IOError as err:
81             raise RuntimeError(
82                 f"Not possible to open the file {self._html_layout_file}\n{err}"
83             )
84
85         try:
86             with open(self._spec_file, "r") as file_read:
87                 self._spec_tbs = load(file_read, Loader=FullLoader)
88         except IOError as err:
89             raise RuntimeError(
90                 f"Not possible to open the file {self._spec_file,}\n{err}"
91             )
92         except YAMLError as err:
93             raise RuntimeError(
94                 f"An error occurred while parsing the specification file "
95                 f"{self._spec_file,}\n"
96                 f"{err}"
97             )
98
99         try:
100             with open(self._graph_layout_file, "r") as file_read:
101                 self._graph_layout = load(file_read, Loader=FullLoader)
102         except IOError as err:
103             raise RuntimeError(
104                 f"Not possible to open the file {self._graph_layout_file}\n"
105                 f"{err}"
106             )
107         except YAMLError as err:
108             raise RuntimeError(
109                 f"An error occurred while parsing the specification file "
110                 f"{self._graph_layout_file}\n"
111                 f"{err}"
112             )
113
114         # Callbacks:
115         if self._app is not None and hasattr(self, 'callbacks'):
116             self.callbacks(self._app)
117
118     @property
119     def html_layout(self):
120         return self._html_layout
121
122     @property
123     def spec_tbs(self):
124         return self._spec_tbs
125
126     @property
127     def data(self):
128         return self._data
129
130     @property
131     def layout(self):
132         return self._graph_layout
133
134     def add_content(self):
135         """
136         """
137         if self.html_layout and self.spec_tbs:
138             return html.Div(
139                 id="div-main",
140                 children=[
141                     dbc.Row(
142                         id="row-navbar",
143                         className="g-0",
144                         children=[
145                             self._add_navbar(),
146                         ]
147                     ),
148                     dbc.Row(
149                         id="row-main",
150                         className="g-0",
151                         children=[
152                             dcc.Store(
153                                 id="selected-tests"
154                             ),
155                             self._add_ctrl_col(),
156                             self._add_plotting_col(),
157                         ]
158                     ),
159                     dbc.Offcanvas(
160                         id="offcanvas-metadata",
161                         title="Throughput And Latency",
162                         placement="end",
163                         is_open=True,
164                         children=[
165                             html.P(
166                                 id="metadata",
167                                 children=[
168                                     "This is the placeholder for metadata."
169                                 ],
170                             )
171                         ]
172                     )
173                 ]
174             )
175         else:
176             return html.Div(
177                 id="div-main-error",
178                 children=[
179                     dbc.Alert(
180                         [
181                             "An Error Occured",
182                         ],
183                         color="danger",
184                     ),
185                 ]
186             )
187
188     def _add_navbar(self):
189         """Add nav element with navigation panel. It is placed on the top.
190         """
191         return dbc.NavbarSimple(
192             id="navbarsimple-main",
193             children=[
194                 dbc.NavItem(
195                     dbc.NavLink(
196                         "Continuous Performance Trending",
197                         external_link=True,
198                         href="#"
199                     )
200                 )
201             ],
202             brand="Dashboard",
203             brand_href="/",
204             brand_external_link=True,
205             #color="dark",
206             #dark=True,
207             fluid=True,
208         )
209
210     def _add_ctrl_col(self) -> dbc.Col:
211         """Add column with controls. It is placed on the left side.
212         """
213         return dbc.Col(
214             id="col-controls",
215             children=[
216                 self._add_ctrl_panel(),
217                 self._add_ctrl_shown()
218             ],
219         )
220
221     def _add_plotting_col(self) -> dbc.Col:
222         """Add column with plots and tables. It is placed on the right side.
223         """
224         return dbc.Col(
225             id="col-plotting-area",
226             children=[
227                 dbc.Row(  # Throughput
228                     id="row-graph-tput",
229                     className="g-0",
230                     children=[
231                         dcc.Loading(
232                             dcc.Graph(id="graph-tput")
233                         )
234                     ]
235                 ),
236                 dbc.Row(  # Latency
237                     id="row-graph-lat",
238                     className="g-0",
239                     children=[
240                         dcc.Loading(
241                             dcc.Graph(id="graph-latency")
242                         )
243                     ]
244                 ),
245                 dbc.Row(  # Download
246                     id="div-download",
247                     className="g-0",
248                     children=[
249                         dcc.Loading(children=[
250                             dbc.Button(
251                                 id="btn-download-data",
252                                 children=["Download Data"]
253                             ),
254                             dcc.Download(id="download-data")
255                         ])
256                     ]
257                 )
258             ],
259             width=9,
260         )
261
262     def _add_ctrl_panel(self) -> dbc.Row:
263         """
264         """
265         return dbc.Row(
266             id="row-ctrl-panel",
267             className="g-0",
268             children=[
269                 dbc.Label("Physical Test Bed Topology, NIC and Driver"),
270                 dcc.Dropdown(
271                     id="dd-ctrl-phy",
272                     placeholder="Select a Physical Test Bed Topology...",
273                     multi=False,
274                     clearable=False,
275                     options=[
276                         {"label": k, "value": k} for k in self.spec_tbs.keys()
277                     ],
278                 ),
279                 dbc.Label("Area"),
280                 dcc.Dropdown(
281                     id="dd-ctrl-area",
282                     placeholder="Select an Area...",
283                     disabled=True,
284                     multi=False,
285                     clearable=False,
286                 ),
287                 dbc.Label("Test"),
288                 dcc.Dropdown(
289                     id="dd-ctrl-test",
290                     placeholder="Select a Test...",
291                     disabled=True,
292                     multi=False,
293                     clearable=False,
294                 ),
295                 dbc.Row(
296                     id="row-ctrl-core",
297                     className="g-0",
298                     children=[
299                         dbc.Label("Number of Cores"),
300                         dbc.Col([
301                             dbc.Checklist(
302                                 id="cl-ctrl-core-all",
303                                 options=[{"label": "All", "value": "all"}, ],
304                                 inline=False,
305                                 switch=False
306                             ),
307                         ], width=3),
308                         dbc.Col([
309                             dbc.Checklist(
310                                 id="cl-ctrl-core",
311                                 inline=True,
312                                 switch=False
313                             )
314                         ])
315                     ]
316                 ),
317                 dbc.Row(
318                     id="row-ctrl-framesize",
319                     className="g-0",
320                     children=[
321                         dbc.Label("Frame Size"),
322                         dbc.Col([
323                             dbc.Checklist(
324                                 id="cl-ctrl-framesize-all",
325                                 options=[{"label": "All", "value": "all"}, ],
326                                 inline=True,
327                                 switch=False
328                             ),
329                         ], width=3),
330                         dbc.Col([
331                             dbc.Checklist(
332                                 id="cl-ctrl-framesize",
333                                 inline=True,
334                                 switch=False
335                             )
336                         ])
337                     ]
338                 ),
339                 dbc.Row(
340                     id="row-ctrl-testtype",
341                     className="g-0",
342                     children=[
343                         dbc.Label("Test Type"),
344                         dbc.Col([
345                             dbc.Checklist(
346                                 id="cl-ctrl-testtype-all",
347                                 options=[{"label": "All", "value": "all"}, ],
348                                 inline=True,
349                                 switch=False
350                             ),
351                         ], width=3),
352                         dbc.Col([
353                             dbc.Checklist(
354                                 id="cl-ctrl-testtype",
355                                 inline=True,
356                                 switch=False
357                             )
358                         ])
359                     ]
360                 ),
361                 dbc.Row(
362                     className="g-0",
363                     children=[
364                         dbc.Button(
365                             id="btn-ctrl-add",
366                             children="Add",
367                         )
368                     ]
369                 ),
370                 dbc.Row(
371                     className="g-0",
372                     children=[
373                         dcc.DatePickerRange(
374                             id="dpr-period",
375                             min_date_allowed=\
376                                 datetime.utcnow()-timedelta(days=180),
377                             max_date_allowed=datetime.utcnow(),
378                             initial_visible_month=datetime.utcnow(),
379                             start_date=datetime.utcnow() - timedelta(days=180),
380                             end_date=datetime.utcnow(),
381                             display_format="D MMMM YY"
382                         )
383                     ]
384                 )
385             ]
386         )
387
388     def _add_ctrl_shown(self) -> dbc.Row:
389         """
390         """
391         return dbc.Row(
392             id="div-ctrl-shown",
393             className="g-0",
394             children=[
395                 dbc.Row(
396                     className="g-0",
397                     children=[
398                         dbc.Label("Selected tests"),
399                         dbc.Checklist(
400                             id="cl-selected",
401                             options=[],
402                             inline=False
403                         )
404                     ]
405                 ),
406                 dbc.Row(
407                     className="g-0",
408                     children=[
409                         dbc.ButtonGroup(
410                             [
411                                 dbc.Button(
412                                     id="btn-sel-remove",
413                                     children="Remove Selected",
414                                     color="secondary",
415                                     disabled=False
416                                 ),
417                                 dbc.Button(
418                                     id="btn-sel-remove-all",
419                                     children="Remove All",
420                                     color="secondary",
421                                     disabled=False
422                                 ),
423                                 dbc.Button(
424                                     id="btn-sel-display",
425                                     children="Display",
426                                     color="secondary",
427                                     disabled=False
428                                 )
429                             ],
430                             size="md",
431                             className="me-1",
432                         ),
433                     ]
434                 )
435             ]
436         )
437
438     def callbacks(self, app):
439
440         @app.callback(
441             Output("dd-ctrl-area", "options"),
442             Output("dd-ctrl-area", "disabled"),
443             Input("dd-ctrl-phy", "value"),
444         )
445         def _update_dd_area(phy):
446             """
447             """
448
449             if phy is None:
450                 raise PreventUpdate
451
452             try:
453                 options = [
454                     {"label": self.spec_tbs[phy][v]["label"], "value": v}
455                         for v in [v for v in self.spec_tbs[phy].keys()]
456                 ]
457                 disable = False
458             except KeyError:
459                 options = list()
460                 disable = True
461
462             return options, disable
463
464         @app.callback(
465             Output("dd-ctrl-test", "options"),
466             Output("dd-ctrl-test", "disabled"),
467             State("dd-ctrl-phy", "value"),
468             Input("dd-ctrl-area", "value"),
469         )
470         def _update_dd_test(phy, area):
471             """
472             """
473
474             if not area:
475                 raise PreventUpdate
476
477             try:
478                 options = [
479                     {"label": v, "value": v}
480                         for v in self.spec_tbs[phy][area]["test"]
481                 ]
482                 disable = False
483             except KeyError:
484                 options = list()
485                 disable = True
486
487             return options, disable
488
489         @app.callback(
490             # Output("row-ctrl-core", "style"),
491             Output("cl-ctrl-core", "options"),
492             # Output("row-ctrl-framesize", "style"),
493             Output("cl-ctrl-framesize", "options"),
494             # Output("row-ctrl-testtype", "style"),
495             Output("cl-ctrl-testtype", "options"),
496             # Output("btn-ctrl-add", "disabled"),
497             State("dd-ctrl-phy", "value"),
498             State("dd-ctrl-area", "value"),
499             Input("dd-ctrl-test", "value"),
500         )
501         def _update_btn_add(phy, area, test):
502             """
503             """
504
505             if test is None:
506                 raise PreventUpdate
507
508             # core_style = {"display": "none"}
509             core_opts = []
510             # framesize_style = {"display": "none"}
511             framesize_opts = []
512             # testtype_style = {"display": "none"}
513             testtype_opts = []
514             # add_disabled = True
515             if phy and area and test:
516                 # core_style = {"display": "block"}
517                 core_opts = [
518                     {"label": v, "value": v}
519                         for v in self.spec_tbs[phy][area]["core"]
520                 ]
521                 # framesize_style = {"display": "block"}
522                 framesize_opts = [
523                     {"label": v, "value": v}
524                         for v in self.spec_tbs[phy][area]["frame-size"]
525                 ]
526                 # testtype_style = {"display": "block"}
527                 testtype_opts = [
528                     {"label": v, "value": v}
529                         for v in self.spec_tbs[phy][area]["test-type"]
530                 ]
531                 # add_disabled = False
532
533             return (
534                 # core_style,
535                 core_opts,
536                 # framesize_style,
537                 framesize_opts,
538                 # testtype_style,
539                 testtype_opts,
540                 # add_disabled
541             )
542
543         def _sync_checklists(opt, sel, all, id):
544             """
545             """
546             options = {v["value"] for v in opt}
547             input_id = callback_context.triggered[0]["prop_id"].split(".")[0]
548             if input_id == id:
549                 all = ["all"] if set(sel) == options else list()
550             else:
551                 sel = list(options) if all else list()
552             return sel, all
553
554         @app.callback(
555             Output("cl-ctrl-core", "value"),
556             Output("cl-ctrl-core-all", "value"),
557             State("cl-ctrl-core", "options"),
558             Input("cl-ctrl-core", "value"),
559             Input("cl-ctrl-core-all", "value"),
560             prevent_initial_call=True
561         )
562         def _sync_cl_core(opt, sel, all):
563             return _sync_checklists(opt, sel, all, "cl-ctrl-core")
564
565         @app.callback(
566             Output("cl-ctrl-framesize", "value"),
567             Output("cl-ctrl-framesize-all", "value"),
568             State("cl-ctrl-framesize", "options"),
569             Input("cl-ctrl-framesize", "value"),
570             Input("cl-ctrl-framesize-all", "value"),
571             prevent_initial_call=True
572         )
573         def _sync_cl_framesize(opt, sel, all):
574             return _sync_checklists(opt, sel, all, "cl-ctrl-framesize")
575
576         @app.callback(
577             Output("cl-ctrl-testtype", "value"),
578             Output("cl-ctrl-testtype-all", "value"),
579             State("cl-ctrl-testtype", "options"),
580             Input("cl-ctrl-testtype", "value"),
581             Input("cl-ctrl-testtype-all", "value"),
582             prevent_initial_call=True
583         )
584         def _sync_cl_testtype(opt, sel, all):
585             return _sync_checklists(opt, sel, all, "cl-ctrl-testtype")
586
587         @app.callback(
588             # Output("graph-tput", "figure"),
589             # Output("graph-latency", "figure"),
590             # Output("div-tput", "style"),
591             # Output("div-latency", "style"),
592             # Output("div-lat-metadata", "style"),
593             # Output("div-download", "style"),
594             Output("selected-tests", "data"),  # Store
595             Output("cl-selected", "options"),  # User selection
596             Output("dd-ctrl-phy", "value"),
597             Output("dd-ctrl-area", "value"),
598             Output("dd-ctrl-test", "value"),
599             State("selected-tests", "data"),  # Store
600             State("cl-selected", "value"),
601             State("dd-ctrl-phy", "value"),
602             State("dd-ctrl-area", "value"),
603             State("dd-ctrl-test", "value"),
604             State("cl-ctrl-core", "value"),
605             State("cl-ctrl-framesize", "value"),
606             State("cl-ctrl-testtype", "value"),
607             Input("btn-ctrl-add", "n_clicks"),
608             Input("btn-sel-display", "n_clicks"),
609             Input("btn-sel-remove", "n_clicks"),
610             Input("btn-sel-remove-all", "n_clicks"),
611             Input("dpr-period", "start_date"),
612             Input("dpr-period", "end_date"),
613             prevent_initial_call=True
614         )
615         def _process_list(store_sel, list_sel, phy, area, test, cores,
616                 framesizes, testtypes, btn_add, btn_display, btn_remove,
617                 btn_remove_all, d_start, d_end):
618             """
619             """
620
621             if not (btn_add or btn_display or btn_remove or btn_remove_all or \
622                     d_start or d_end):
623                 raise PreventUpdate
624
625             def _list_tests():
626                 # Display selected tests with checkboxes:
627                 if store_sel:
628                     return [
629                         {"label": v["id"], "value": v["id"]} for v in store_sel
630                     ]
631                 else:
632                     return list()
633
634             class RetunValue:
635                 def __init__(self) -> None:
636                     self._output = {
637                         # "graph-tput-figure": no_update,
638                         # "graph-lat-figure": no_update,
639                         # "div-tput-style": no_update,
640                         # "div-latency-style": no_update,
641                         # "div-lat-metadata-style": no_update,
642                         # "div-download-style": no_update,
643                         "selected-tests-data": no_update,
644                         "cl-selected-options": no_update,
645                         "dd-ctrl-phy-value": no_update,
646                         "dd-ctrl-area-value": no_update,
647                         "dd-ctrl-test-value": no_update,
648                     }
649
650                 def value(self):
651                     return tuple(self._output.values())
652
653                 def set_values(self, kwargs: dict) -> None:
654                     for key, val in kwargs.items():
655                         if key in self._output:
656                             self._output[key] = val
657                         else:
658                             raise KeyError(f"The key {key} is not defined.")
659
660
661             trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
662
663             d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
664                 int(d_start[8:10]))
665             d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
666
667             output = RetunValue()
668
669             if trigger_id == "btn-ctrl-add":
670                 # Add selected test to the list of tests in store:
671                 if phy and area and test and cores and framesizes and testtypes:
672                     if store_sel is None:
673                         store_sel = list()
674                     for core in cores:
675                         for framesize in framesizes:
676                             for ttype in testtypes:
677                                 tid = (
678                                     f"{phy.replace('af_xdp', 'af-xdp')}-"
679                                     f"{area}-"
680                                     f"{framesize.lower()}-"
681                                     f"{core.lower()}-"
682                                     f"{test}-"
683                                     f"{ttype.lower()}"
684                                 )
685                                 if tid not in [itm["id"] for itm in store_sel]:
686                                     store_sel.append({
687                                         "id": tid,
688                                         "phy": phy,
689                                         "area": area,
690                                         "test": test,
691                                         "framesize": framesize.lower(),
692                                         "core": core.lower(),
693                                         "testtype": ttype.lower()
694                                     })
695                 output.set_values({
696                     "selected-tests-data": store_sel,
697                     "cl-selected-options": _list_tests(),
698                     "dd-ctrl-phy-value": None,
699                     "dd-ctrl-area-value": None,
700                     "dd-ctrl-test-value": None,
701                 })
702
703             elif trigger_id in ("btn-sel-display", "dpr-period"):
704                 fig_tput, fig_lat = graph_trending(
705                     self.data, store_sel, self.layout, d_start, d_end
706                 )
707                 # output.set_values({
708                 #     "graph-tput-figure": \
709                 #         fig_tput if fig_tput else self.NO_GRAPH,
710                 #     "graph-lat-figure": \
711                 #         fig_lat if fig_lat else self.NO_GRAPH,
712                 #     "div-tput-style": \
713                 #         self.STYLE_BLOCK if fig_tput else self.STYLE_HIDEN,
714                 #     "div-latency-style": \
715                 #         self.STYLE_BLOCK if fig_lat else self.STYLE_HIDEN,
716                 #     "div-lat-metadata-style": \
717                 #         self.STYLE_BLOCK if fig_lat else self.STYLE_HIDEN,
718                 #     "div-download-style": \
719                 #         self.STYLE_BLOCK if fig_tput else self.STYLE_HIDEN,
720                 # })
721             elif trigger_id == "btn-sel-remove-all":
722                 output.set_values({
723                     "selected-tests-data": list(),
724                     "cl-selected-options": list()
725                 })
726             elif trigger_id == "btn-sel-remove":
727                 if list_sel:
728                     new_store_sel = list()
729                     for item in store_sel:
730                         if item["id"] not in list_sel:
731                             new_store_sel.append(item)
732                     store_sel = new_store_sel
733                 if store_sel:
734                     fig_tput, fig_lat = graph_trending(
735                         self.data, store_sel, self.layout, d_start, d_end
736                     )
737                     output.set_values({
738                         # "graph-tput-figure": \
739                         #     fig_tput if fig_tput else self.NO_GRAPH,
740                         # "graph-lat-figure": \
741                         #     fig_lat if fig_lat else self.NO_GRAPH,
742                         # "div-tput-style": \
743                         #     self.STYLE_BLOCK if fig_tput else self.STYLE_HIDEN,
744                         # "div-latency-style": \
745                         #     self.STYLE_BLOCK if fig_lat else self.STYLE_HIDEN,
746                         # "div-lat-metadata-style": \
747                         #     self.STYLE_BLOCK if fig_lat else self.STYLE_HIDEN,
748                         # "div-download-style": \
749                         #     self.STYLE_BLOCK if fig_tput else self.STYLE_HIDEN,
750                         "selected-tests-data": store_sel,
751                         "cl-selected-options": _list_tests()
752                     })
753                 else:
754                     output.set_values({
755                         # "graph-tput-figure": self.NO_GRAPH,
756                         # "graph-lat-figure": self.NO_GRAPH,
757                         # "div-tput-style": self.STYLE_HIDEN,
758                         # "div-latency-style": self.STYLE_HIDEN,
759                         # "div-lat-metadata-style": self.STYLE_HIDEN,
760                         # "div-download-style": self.STYLE_HIDEN,
761                         "selected-tests-data": store_sel,
762                         "cl-selected-options": _list_tests()
763                     })
764
765             return output.value()
766
767         # @app.callback(
768         #     Output("tput-metadata", "children"),
769         #     Input("graph-tput", "clickData")
770         # )
771         # def _show_tput_metadata(hover_data):
772         #     """
773         #     """
774         #     if not hover_data:
775         #         raise PreventUpdate
776
777         #     return hover_data["points"][0]["text"].replace("<br>", "\n")
778
779         # @app.callback(
780         #     Output("graph-latency-hdrh", "figure"),
781         #     Output("graph-latency-hdrh", "style"),
782         #     Output("lat-metadata", "children"),
783         #     Input("graph-latency", "clickData")
784         # )
785         # def _show_latency_hdhr(hover_data):
786         #     """
787         #     """
788         #     if not hover_data:
789         #         raise PreventUpdate
790
791         #     graph = no_update
792         #     hdrh_data = hover_data["points"][0].get("customdata", None)
793         #     if hdrh_data:
794         #         graph = graph_hdrh_latency(hdrh_data, self.layout)
795
796         #     return (
797         #         graph,
798         #         self.STYLE_INLINE,
799         #         hover_data["points"][0]["text"].replace("<br>", "\n")
800         #     )
801
802         # @app.callback(
803         #     Output("download-data", "data"),
804         #     State("selected-tests", "data"),
805         #     Input("btn-download-data", "n_clicks"),
806         #     prevent_initial_call=True
807         # )
808         # def _download_data(store_sel, n_clicks):
809         #     """
810         #     """
811
812         #     if not n_clicks:
813         #         raise PreventUpdate
814
815         #     df = pd.DataFrame()
816         #     for itm in store_sel:
817         #         sel_data = select_trending_data(self.data, itm)
818         #         if sel_data is None:
819         #             continue
820         #         df = pd.concat([df, sel_data], ignore_index=True)
821
822         #     return dcc.send_data_frame(df.to_csv, "trending_data.csv")