Black Lives Matter. Please consider donating to Black Girls Code today.
Learn about the upcoming Dash Enterprise 4.0 release in the August 5th webinar with Chris Parmer, the Inventor of Dash.

Show and Tell - Download Component

Facilitated by the need to enable download of in-memory data, i wrote a Download component some time ago. I have recently updated the code and added a few examples to the documentation. As there has been a number of question on the forum related to downloading data, files, and pandas data frame, i figured it might be of interest to others.

As of version 0.0.18, the basic syntax is like this,

import dash
import dash_html_components as html
from dash.dependencies import Output, Input
from dash_extensions import Download

app = dash.Dash(prevent_initial_callbacks=True)
app.layout = html.Div([html.Button("Download", id="btn"), Download(id="download")])

@app.callback(Output("download", "data"), [Input("btn", "n_clicks")])
def func(n_clicks):
    return dict(content="Hello world!", filename="hello.txt")

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

That is, you just add the component anywhere in the layout (like a Store) and a callback which targets its data property. The data must be in the form of a dictionary with keys content (a string) and filename. To ease the download of files, i have included a file utility function,

import dash
import dash_html_components as html  
from dash.dependencies import Output, Input
from dash_extensions import Download
from dash_extensions.snippets import send_file

app = dash.Dash(prevent_initial_callbacks=True)
app.layout = html.Div([html.Button("Download", id="btn"), Download(id="download")])

@app.callback(Output("download", "data"), [Input("btn", "n_clicks")])
def func(n_clicks):
    return send_file("/home/emher/Documents/Untitled.png")

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

It takes the file path and optionally a filename (if you want to rename the file on download). Finally, i have included a utility method for data frames,

import dash
import pandas as pd
import dash_html_components as html

from dash.dependencies import Output, Input
from dash_extensions import Download
from dash_extensions.snippets import send_data_frame

# Example data.
df = pd.DataFrame({'a': [1, 2, 3, 4], 'b': [2, 1, 5, 6], 'c': ['x', 'x', 'y', 'y']})
# Create app.
app = dash.Dash(prevent_initial_callbacks=True)
app.layout = html.Div([html.Button("Download", id="btn"), Download(id="download")])

@app.callback(Output("download", "data"), [Input("btn", "n_clicks")])
def func(n_nlicks):
    return send_data_frame(df.to_excel, "mydf.xls", index=False)
 
if __name__ == '__main__':
    app.run_server()

I haven’t tested it extensively, but it should support all pandas file formats except SQL, i.e. json, html, hdf, feather, parquet, megpack, stata and pickle.

I am currently considering moving the Download component to a separate package, but for the time being, it is part of dash_extensions. If you would like to try it out, it can be installed via pip,

pip install dash-extensions==0.0.18

Happy downloading :slightly_smiling_face:

6 Likes

Great work and much needed! Thanks so much for publishing, documenting, and announcing :tada:

1 Like

@Emil, did you make the renaming work for send_data_frame() yet because the following does not rename for me.

import base64, io
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
from dash.exceptions import PreventUpdate
import pandas as pd
from os.path import join as pjoin
from dash_extensions import Download
from dash_extensions.snippets import send_data_frame

app = dash.Dash(__name__)

app.layout = html.Div([

    # input file is selected from a dialogue box
    dcc.Upload(
        id='csv',
        children=html.Div([
            'Drag and Drop or ',
            html.A('Select Files')
        ]),
        style={
            'width': '30%',
            'height': '40px',
            'lineHeight': '40px',
            'borderWidth': '1px',
            'borderStyle': 'dashed',
            'borderRadius': '5px',
            'textAlign': 'center',
        },
    ),

    html.Br(),
    html.Br(),
    'Enter output filename ',
    # FIXME can output directory be selected from a dialogue box ?
    dcc.Input('outfile'),
    html.Div([html.Button("Download", id="btn"), Download(id="download")]),

    html.Br(),
    html.Div(id='page-content')

])

@app.callback([Output('page-content','children'), Output('download','data')],
              [Input('csv','contents'), Input('btn','n_clicks'), Input('outfile','value')])
def update_dropdown(raw_contents, activate, outfile):

    if not activate:
        raise PreventUpdate

    _, contents = raw_contents.split(',')
    decoded = base64.b64decode(contents)
    df = pd.read_csv(io.StringIO(decoded.decode('utf-8')))

    obj= send_data_frame(df.to_csv, outfile, index=False)

    return ('File loaded, saving ...', obj)


if __name__=='__main__':
    app.run_server(debug=True, port=8050, host='localhost')

I provided /home/emher/Documents/Untitled.csv as output name but, it replaces the / with __ only and not download it to the desired location. Working on windows though!

The filename is just the name - not the path. Hence it should be something like “mycsv.csv”, not “/home/emher/Documents/mycsv.csv”.

Sure, that means renaming won’t work for send_data_frame though it works for send_file.

No, in all cases filename refers only to the name. In the case of send_file, the path provided is to the file on the server, and the filename argument (which is what is suggested in the download dialogue) defaults to the original filename.