Allowing users to download CSV on click

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 - #8 by chriddyp without doing this in a @server.route.

6 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)
6 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()
1 Like

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.

1 Like

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