Allowing users to download CSV on click

@ PatrickGlettig

Hi Patrick does your approach needs any update if we are using it in an app being deployed on the on premise plotly server. Because the method is working fine on the local app but if we do it on the server, it is actually taking to a URL and not downloading the csv

I need help to update this with python 3. Thank you very much.

As a twist on these you can also allow users to Download Data selected on a graph

# return everything
def selected_data_to_csv(selected_data_dict): 
    return pd.DataFrame(selected_data_dict['points']).to_csv()
# just return x,y axis (or extend to any other given in the graph)
def selected_data_to_csv(selected_data_dict, xaxis_name, yaxis_name):
    csv_string = "description,{},{}".format(xaxis_name, yaxis_name) + linesep 
    csv_string = "{}{}".format(
        csv_string, 
        "{}".format(linesep).join([
            '{}, {}, {}'.format(point['text'], point['x'], point['y']) 
            for point in selected_data_dict['points']
        ])
    )
    return csv_string
import urllib.parse
@app.callback(
    dash.dependencies.Output('download-selection', 'href'),
    [
        dash.dependencies.Input('indicator-graphic', 'selectedData'),
        dash.dependencies.Input('xaxis-column', 'value'),
        dash.dependencies.Input('yaxis-column', 'value')
    ])
def download_selected(selected_data, xaxis_name, yaxis_name):
    if type(selected_data) == dict:
        ret_str = "{}{}".format(
            "data:text/csv;charset=utf-8,%EF%BB%BF", 
            urllib.parse.quote(
                selected_data_to_csv(selected_data, xaxis_name, yaxis_name), encoding="utf-8"
            )
        )
    else:
        ret_str = ""
    return ret_str

and just add in a link where you want

# Download Selection
html.A(
    'Download *SELECTED* Data',
    id='download-selection',
    href="",
    target="_blank"
),

The download function should be a standalone component.

There are two reasons.

All of the above discussions are just writing callback functions in different patterns, and it is the Flask.server.route function that implements this function. But this function is not controlled by Dash-auth.

I use add random numbers as a temporary solution to ensure a certain level of permission control. However, when you usually provide a download link for a file, you also need to consider a series of issues related to link validity, anti-leech, server pressure, storage space, and so on.

To deal with so many transactions, is there no need to specifically develop a component?

Reason two.

As far as I know, Dash is not a framework for file sharing. To download CSV, it would be nice to put it in a shared folder.

So what is the Dash way to download files? I think it’s WYSIWYG. When making a page, those tabular styles are also well thought out and have important significance. Wouldn’t it be scary to turn into a bunch of dense numbers when sharing offline reports?

Pdf? Csv? No. Must be xlsx, HTML, those that can not only be seen in style, but can also be imported into other tools.

1 Like

Is there a way to download data from a ‘Store’ object? All these examples seem to use a dataframe as a global variable, so the download_csv() function doesn’t have any issue pulling it into its scope.

My issue is that I have JSON data in a Store object (some people use a hidden Div) where the data will change depending on the user input. The data’s too big to just change the href of the download link, but I don’t see how I can make the @app.server.route callback aware of the Store object I am using.

Sorry if what I’m asking doesn’t make a lot of sense. Still new to this.

2 Likes

Hello,
when users click a download button (or any other event), you need to save your data to filesystem (csv, json 
) first, and then open the download url (in @app.server.route) for the users.
you can use visdcc.Run_js to write your javascript. For example, you can open download url for the users using window.open javascript function. :slightly_smiling_face:

hey,

I am curious if this changes up when the dataframe is big. Does this work for only small dataframes or big ones as well? I have a structure, but for some reason, it works only after the first click.

@app.callback(Output(‘download-link’, ‘href’),
[Input(component_id=‘date-picker’,component_property=‘start_date’),
Input(component_id=‘date-picker’,component_property=‘end_date’),
Input(component_id=‘spread-picker’,component_property=‘value’),
Input(component_id=‘index-picker’,component_property=‘value’),
Input(component_id=‘download-link’,component_property=‘n_clicks’)])
def download_data(starting_date,ending_date,selected_spread,selected_indicies,clicks):
if clicks > 0:
dff = df[(df[‘ReportDate’] >= pd.to_datetime(starting_date)) & (df[‘ReportDate’] <= pd.to_datetime(ending_date)) & (df[‘TickerIndex’].isin(selected_indicies))][[‘ReportDate’,‘TickerIndex’,selected_spread]].reset_index(drop=True)
downloadable_df = None
for index in selected_indicies:
temp_df = dff[dff[‘TickerIndex’]==index][[‘ReportDate’,selected_spread]].rename(columns={selected_spread:index}).reset_index(drop=True)
if downloadable_df is None:
downloadable_df = temp_df
else:
downloadable_df = pd.merge(downloadable_df,temp_df,how=‘outer’,on=‘ReportDate’).reset_index(drop=True)
csv_string = downloadable_df.to_csv(index=False, encoding=‘utf-8’)
csv_string = “data:text/csv;charset=utf-8,” + parse.quote(csv_string)
return csv_string
else:
return ‘’

Hey Dayxx369,

Not sure I understand your question properly, but I give it a try.
That it only happens after the first click might be a problem from n_clicks becoming clicks, then the conditional if clicks > 0, and otherwise returning an empty string which should not be recognized as a valid download link.
Hope this helps. Otherwise please try formatting your code and stripping out the relevant portion to help better.

1 Like

Wow, cannot believe that I didn’t remove that before. It works perfectly now. Thanks

@app.callback(
dash.dependencies.Output(‘output-container’, ‘children’),
[dash.dependencies.Input(‘my-dropdown’, ‘value’)])
def update_output(value):
df1 = pd.read_csv(‘returns_summary.csv’)
df2 = pd.read_csv(‘ReturnGuideDocDetails.csv’)

if value == 'df1':
    return generate_table(df1)
elif value == 'df2':
    return generate_table(df2)

@app.callback(Output(‘my-link’, ‘href’), [Input(‘my-dropdown’, ‘value’)])
def update_link(value):
return ‘/dash/urlToDownload?value={}’.format(value)

@app.server.route(’/dash/urlToDownload’)
def download_csv():
value = flask.request.args.get(‘value’)
strIO = StringIO()
strIO.write(‘You have selected {}’.format(value))
strIO.seek(0)

if value == 'df1':
    return send_file(strIO,
                     mimetype='text/csv',
                     attachment_filename='returns_summary.csv',
                     as_attachment=True)
elif value == 'df2':
    return send_file(strIO,
                     mimetype='text/csv',
                     attachment_filename='ReturnGuideDocDetails.csv',
                     as_attachment=True)

Hey, am trying the above code but on downloading the csv, its giving the value (df1/df2 as values) instead of the csv. what do i change to download the csv ?

Hey Chris,

Is there a way to access the state of other dash components from within the routed function, without explicitly creating and managing user cookies/sessions?
I understand how to send one, two or any fixed number of variables using the routing procedure and extract them from the request, but I’ve been trying to access other information that might have flexible lengths that could exceed the maximum href length for certain browsers (e.g. large lists of vertices from lasso selection).

Hey,

I tried gerdekk’s code (really helpful!) but got an issue. Here is my similar code:

app.layout = html.Div(children=[
      html.Div(children=[
              html.A("Générer !", href="/download_excel/")
              ])                           
])

@app.server.route('/download_excel/')
def download_csv():
    #Convert DF
    str_io = io.StringIO()
    df[0:10].to_csv(str_io)
    mem = io.BytesIO()
    mem.write(str_io.getvalue().encode('utf-8'))
    mem.seek(0).close()
    return flask.send_file(mem,
                       mimetype='text/csv',
                       attachment_filename='downloadFile.csv',
                       as_attachment=True)

This code is working well but generate a xlsx file.

Being bad in coding, I just tryed to replace “/download_excel/” (found on another topic) by “/dash/url download”, “/download” or “/dash/urlToDownload” (I tryed different things found somewhere with Google). But apparently it does not work that way :sweat_smile:

For example, “/dash/urlToDownload” leads me to http://127.0.0.1:8050/dash/urlToDownload with that error "AttributeError: ‘int’ object has no attribute ‘close’ ". I actually got the same error for my 3 tests.

I’m not sure how @app.server.route works
?

I assume “/download_excel/” is the reason why I got xlsx instead of csv. How could I fix that issue? Thank you!

I meant to come back to this thread based on the comment from @stu. Definitely caused some alarm discussing authentication/routing permissions. Our apps implement OAuth directly using flask-dance (instead of dash-auth), so it didn’t end up being a problem.

However, the thought about WYSIWYG, and link validity/server pressure/storage space stuck with me, along with the obvious desire to have filterable downloads (seems like people have been creating pretty complex routes!). I’ve come up with an alternative client-based solution that addresses those problems. I use it now when handling large real-time IoT data streams. I put together a basic working demo this holiday weekend, which is now running off of a free heroku instance: https://dash-clientside-demo.herokuapp.com
Repository is on github.

Summary: the app uses FileSaverJS to create a blob out of data accessible in the browser application. It’s implemented with clientside callbacks so large data overhead doesn’t need to pass between the server and client an extra time. Particularly important when talking about large sets of processed data that might cause your worker threads to time out. Note that FileSaverJS also eliminates the browser download caps discussed in this thread.

NOTE: 
 older browsers may not be compatible. FileSaver requires browsers to support the Blob structure. Here’s the project’s Supported Browsers list.

In the example, data comes directly from figure.data (so
 not exactly the same as saving a pandas df). It’s just grabbing the trace data from a time series scattergl trace. There’s no reason you can’t generate a blob/file directly from any object that could be passed in via callback. In some cases the data doesn’t even need to be passed in. Some examples:

  • save selected data from a figure by callback: Input('figure-id', 'selectedData') @rccg
  • directly access session storage via window.sessionStorage.getItem(..)instead of sending the contents of dcc.Store in a callback @praetor
  • If a static file really needs to be served, you could place it in a web-accessible path (e.g. /assets) and access them through an await fetch() call (warning: not secure)
...
# FileSaverJS supports 500MB downloads from the browser!
app.clientside_callback(
    ClientsideFunction('download', 'csvDownload'),
    Output('button-csv-target', 'children'),
    [Input('button-csv-download', 'n_clicks')],
    [State('btc-signal', 'figure')]
)
4 Likes

I got this to work on chrome, but for some reason it doesn’t work on Microsoft Edge. Has anyone had any luck getting it to work on Edge (or Safari, for that matter, which I haven’t checked yet?)

I also seem to be having a problem where files are being cached. When I update my code (so that the data is changed) and click the download CSV button, I get the old version of the file until I manually clear my browser’s cache. I’ve tried using Flask-Caching to clear this automatically:

cache = Cache(app.server, config={
‘CACHE_TYPE’: ‘simple’
})

cache.clear()

But it doesn’t seem to be working. I am storing the data in a hidden div to share between callbacks, but I don’t think that’s what’s being cached, because one of my code changes didn’t modify the hidden div data at all, just how the hidden div data was processed before writing to csv.

EDIT: I was able to fix the caching/not updating issue by using @carlottanegri and @chriddyp’s solution. However, it still doesn’t work on Edge.

@mathbusters Are you talking about the flask-based routing that uses send_file() in python, or the FileSaverJS implementation I posted? I can’t answer your question on the flask approach, but I did check into it for the clientside javascript solution. It turns out that the current release for the Edge browser (EdgeHTML engine v18) doesn’t fully support the File API (https://caniuse.com/#feat=fileapi). Specifically, it doesn’t support theFile constructor, which is used in building the CSV content prior to download.

From /assets/app-download.js#L64:

        // generate file and send through file-saver
        const file = new File([dataTable.concat("\n\n", footer)], "bpi.csv", {
            type: "text/csv;charset=utf-8"
        });

On the other hand, the Blob object is more fully supported across browsers and versions (https://caniuse.com/#search=Blob)

I should’ve looked more deeply into it. I obviously didn’t do the requisite testing on alternate browsers (Edge isn’t used here at work). I’ll create an issue on github with a plan to migrate from using File() to Blob() on github, probably tackle it this weekend.

@mathbusters

The demo app has been changed as I described. I tested it with Edge on a windows 10 machine (Microsoft Edge 44.17763.1.0, Microsoft EdgeHTML 18.17763)

See assets/app-download.js#L65-L70 for the relevant changes in the repo.

Where is the send_file() coming from???

Is there a way to incorporate the dcc.loading component with the app.server.route? I have a large csv file that is returned with flask.send_file but when I click the link that is passed into app.server.route there is no indication of the progress. I understand how to do this for a callback but not server.route, if it’s possible.

from flask import send_file

I’m a bit late to this but I thought’d I’d add a small snippet showing how to include multiple arguments in the href and then read those arguments to assemble your csv. In this example we need two arguments to create a dataframe, product and city. Additionally, city is a list of strings describing the cities we’ve selected.

This stackexchange post was useful in figuring out how to encode the arguments.

import io
import urllib

import flask
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Output, Input
import pandas as pd

@app.callback(
    [
        Output(component_id="download-link", component_property="href"),
    ],
    [
        Input(component_id="product-dropdown", component_property="value"),
        Input(component_id="city-dropdown", component_property="value"),
    ]
)
def update_link(product, city):
    query_params = {"product": product, "city": city}
    query_string = urllib.parse.urlencode(query_params, doseq=True)
    return f"/dash/urlToDownload?{query_string}"


@app.server.route('/dash/urlToDownload')
def download_csv():
    product = flask.request.args.get("product")
    city = flask.request.args.getlist("city")
    testcsv = testdf[
        (testdf["Product"] != product)
        & testdf["City"].isin(city)
    ]
    str_io = io.StringIO()
    testcsv.to_csv(str_io)

    mem = io.BytesIO()
    mem.write(str_io.getvalue().encode('utf-8'))
    mem.seek(0)
    str_io.close()
    return flask.send_file(mem,
                           mimetype='text/csv',
                           attachment_filename='downloadFile.csv',
                           as_attachment=True,
                           cache_timeout=0
    )