Allowing users to download CSV on click

My dataframes are large and i have limited my output to df[0:20] for faster web browsing by users. However, in several cases a user needs to download the full df as a csv. Can I enable this via click of a dcc.RadioItems or any other way?

2 Likes

Use Html Link component (from dash-html-components) to provide link for the csv file.

I’ve been trying to do this and have had no luck. I cannot see how to do this with a link component. I have also tried with dcc.Markdown and html.A components – all with no luck.

I can link to an external site with dcc.Markdown, but not to a csv file in a relative path.

Anyone have success with this?

Update: Using the flask server route, allows me to download a csv file.

@app.server.route('/dash/urlToDownload') 
def download_csv():
    return send_file('output/downloadFile.csv',
                     mimetype='text/csv',
                     attachment_filename='downloadFile.csv',
                     as_attachment=True)
4 Likes

Thanks, this was really helpful!

There had been a discussion about this:


It worth reading it if the data to be downloaded is not very big.

2 Likes

this is a nice solution but it just works for “static” data.

the route callback gets just eveluated once. so you have no chance to update the data that is served in the callback

I believe that you can update this by adding URL parameters to the href property of your html.A component.

For example:

@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')
    # create a dynamic csv or file here using `StringIO` 
    # (instead of writing to the file system)
    strIO = StringIO.StringIO()
    strIO.write('You have selected {}'.format(value)) 
    strIO.seek(0)    
    return send_file(strIO,
                     mimetype='text/csv',
                     attachment_filename='downloadFile.csv',
                     as_attachment=True)

but of course, if your file is small, you can just fill out the href property directly as in Download raw data without doing this in a @server.route.

4 Likes

I’m a complete beginner, please pardon the basicness of my questions. :sweat:

I don’t understand how to incorporate this into an app, mostly because I don’t understand how @app.server.route works and how it links to the other parts of the app.

Would it be possible to post a short example of a complete app?

And do you have any recommendations of where I can learn more about @app.server.route? When I google I find some examples but no explanations.

Thanks for your patience. :slightly_smiling_face:

In a Dash app, the server attribute in app.server corresponds to a Flask instance. So if you were looking at a Flask tutorial (such as the Flask Quickstart guide), @app.server.route would be likely to be presented as @app.route. It’s a little confusing, but the variable name app is simply a convention. In Dash apps people typically use it to contain a Dash app (with a Flask instance on the server attribute), whereas in Flask apps app usually contains the Flask instance.

Hi @chriddyp, I don’t understand the layout components that would correspond to these callbacks.

Would it be something like…

app.layout = html.Div([
    dcc.Dropdown(id='my-dropdown', value = 'default',
        options =[
           {'label': 'New York City', 'value': 'NYC'},
           {'label': 'Montréal', 'value': 'MTL'},
           {'label': 'San Francisco', 'value': 'SF'}
        ]
    ),
    dcc.Link('Download CSV', id = 'my-link'),

])

It would be

app.layout = html.Div([
    dcc.Dropdown(id='my-dropdown', value = 'default',
        options =[
           {'label': 'New York City', 'value': 'NYC'},
           {'label': 'Montréal', 'value': 'MTL'},
           {'label': 'San Francisco', 'value': 'SF'}
        ]
    ),
    html.A('Download CSV', id = 'my-link'),

])

I had some trouble getting this to work in Python 3. After some searching I found out that some minor changes were needed (ref https://stackoverflow.com/questions/35710361/python-flask-send-file-stringio-blank-files). Taking together the above gave me the following working example:

import io

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

app = dash.Dash(__name__)

app.layout = html.Div([
    dcc.Dropdown(id='my-dropdown', value='default',
                 options=[
                     {'label': 'New York City', 'value': 'NYC'},
                     {'label': 'Montréal', 'value': 'MTL'},
                     {'label': 'San Francisco', 'value': 'SF'}
                 ]
                 ),
    html.A('Download CSV', id='my-link'),

])


@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')
    # create a dynamic csv or file here using `StringIO`
    # (instead of writing to the file system)
    str_io = io.StringIO()
    str_io.write('You have selected {}'.format(value))
    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)


if __name__ == '__main__':
    app.run_server(debug=True)
4 Likes

Thanks for this example. However, for the life of me, I cannot seem to apply it downloading a dataframe. I have read the flask quickstart page, read up on stringIO, and basically spent most of a day trying different approaches. In fact, I think I have made it much more complicated than it probably is. I think that my hangup is in passing a df to the @app.server.route function.

Would it be possible to provide another example?

Thanks in advance.

Not sure whether I understand your question, maybe you can give an example?

For now I assume you want to download a dataset as csv, which is a dataframe in your app. Do you have to pass your dataframe to the route function, or could you just get it locally in your function? Where does your dataframe come from? If it is a call to a database, you can just do the call in the route function. Is it available in another place in the app? Then, maybe look at https://dash.plot.ly/sharing-data-between-callbacks for some ideas.

Hope this helps,

Thanks so much for the fast response. The fact that you didnt’ understand my question confirmed that I must be missing something really fundamental. So, I went back and did some more work and solved it! The one remaining question is: Is it possible to use multiple inputs (Region, Country, etc.)? And, if so , how? Thanks again.

import io

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 = dash.Dash(name)

app.layout = html.Div([
dcc.Dropdown(id=‘my-dropdown’, value=‘default’,
options=[
{‘label’: ‘New York City’, ‘value’: ‘NYC’},
{‘label’: ‘Montréal’, ‘value’: ‘MTL’},
{‘label’: ‘San Francisco’, ‘value’: ‘SF’}
]
),
html.A(‘Download CSV’, id=‘my-link’),

])

testdf = pd.read_csv(‘C:/cities.csv’,encoding=‘ISO-8859-1’)

@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():This text will be hidden
value = flask.request.args.get(‘value’)
testcsv = testdf[testdf[‘City’]!=value]

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)

if name == ‘main’:
app.run_server(debug=True)

I want to share data between callbacks. I am producing and a big dataframe that I am using to create some plots and I want to allow users to download the DF.
How I will ‘transfer’ the DF to flask to be able to download it?

Thanks!

@app.callback(Output(‘signal’, ‘children’),[Input(‘my-dropdown’, ‘value’)])
def DF(value):
#Many calculations
test_df = pd.DataFrame(index = [], columns = [‘test’]) #Big DataFrame
datasets[‘Join’] = test_df.to_json(orient=‘split’, date_format=‘iso’)
ar_global = json.dumps(datasets)
return ar_global

@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’)

#DF from signal

strIO = StringIO.StringIO()
strIO.write('You have selected {}'.format(value)) 
strIO.seek(0)    
return send_file(strIO,
                 mimetype='text/csv',
                 attachment_filename='downloadFile.csv',
                 as_attachment=True)

I have figured out a solution on how to use multiple filters. I believe it is straightforward but for a newbie like me it was quite hard to come up with it. So I thought I share it in case somebody else comes along:

In the front end I have a download button:

import os
import pandas as pd
import io
import flask
import dash
import dash_core_components as dcc
import dash_html_components as html
from datetime import datetime as dt

app = dash.Dash()
app.css.append_css({'external_url': 'https://codepen.io/chriddyp/pen/bWLwgP.css'})
app.layout = html.Div([
html.A(html.Button('Download', id='download-button'), id='download-link')])

Note that you will need to add your dcc components above to filter the dataframe (in my case: gender-picker, joindate and state-picker). The way I do it from here is to merge all my filters on the dataframe in the link like this:

@app.callback(
    dash.dependencies.Output('download-link', 'href'),
    [dash.dependencies.Input('gender-picker', 'values'),
     dash.dependencies.Input('joindate', 'start_date'),
     dash.dependencies.Input('state-picker', 'values')])
    
def update_link(selected_gender, join_start_date, join_end_date, selected_state):
    return '/dash/urlToDownload?value={}/{}/{}'.format('-'.join(selected_gender),
                                                                  dt.strptime(join_start_date,'%Y-%M-%d'),
                                                                  '-'.join(selected_state))

Then I get the value and decompose it in its single parts which I then use to subset my dataframe:

@app.server.route('/dash/urlToDownload')
def download_csv():
    value = flask.request.args.get('value')
#here is where I split the value
    value = value.split('/')
    
    selected_gender = value[0].split('-')
    selected_state = value[2].split('-')
    join_start_date = value[1]
    
    filtered_df = df[df['Gender'].isin(selected_gender)]
    filtered_df = filtered_df[filtered_df['zip_state'].isin(selected_state)]
    filtered_df = filtered_df.loc[(demographics['JoinDate'] >= join_start_date),]
    
    #if you use Python 3, use io.StringIO()
    str_io = io.BytesIO()
    filtered_df.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)

if __name__ == '__main__':
    app.run_server()

The example I gave above gives all arguments to the Flask route it needs to recreate a data frame. The idea is to within the route be able to re-filter the data matching the dashboard. This can get problematic if you data is large, expensive to compute/query, etc - so in cases where you would typically share data between callbacks using a hidden Div as explained in the Dash tutorial.

In such a case, another example provided by Chris could be an alternative: https://github.com/plotly/dash-recipes/blob/master/dash-download-file-link-server.py. Here you see how you could use a callback to create a file on disk, and a Flask route serving out the file. This proved to be a better solution for me in some use cases.
Some adaptations I did in addition to the example:

  • I use Pythons uuid library to create unique file names when I save to file and update the link to ensure the link does not point to an old file. And of course I use pathlib.Path instead of os.path. Example of what this could look like:
from uuid import uuid4
from pathlib import Path

fname = Path("downloads") / f"{uuid4()}.xlsx"
ffname = Path.cwd() / fname
Path.mkdir(ffname.parent, parents=True, exist_ok=True)
with open(... etc.)

return f"/{fname}"

Hope this helps!

1 Like

And on the hidden div use case: seems there is now a dedicated component for that use case called dcc.Store: https://dash.plot.ly/dash-core-components/store which is not referenced yet in the tutorial as of for now.