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