Black Lives Matter. Please consider donating to Black Girls Code today.

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:

10 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.

Thanks a lot @Emil for posting this. I was looking for the script to download the dataframe but couldn’t find any, this code really helped. Is there a way that we can keep the formatting intact while downloading the dashtable from web app. I am displaying the dataframe as a dash datatable in my web app and would like to keep the formatting while downloading, not sure if it’s possible. Also, is it possible to download the dataframe as PDF/html?

Great to hear that it helped! The Download component supports any file type (i.e. also PDF/html), but it does not include any functionality to convert data frames into PDF/html. Hence for this purpose, you should look at other libraries :slight_smile:

Hi @Emil!
Thanks for this component - question for you. This works perfectly when I’m running it locally, but when it’s deployed to a server, the download appears to go nowhere - I can’t find any error messages. My guess is it’s a permissions issue or downloading to a folder somewhere on the server. Do you have any tips on how to change the routing for the downloads to the user’s downloads folder or print where it’s saving the downloaded file to? I’m not an expert on the deployment side of things, so any tips you have would be great.
Thanks for your help!

Are you downloading files or in-memory data? Could you share a small/pseudo code example?

Ah - maybe that’s the issue? Since it works perfectly locally, I assumed it was a permissions/path issue. It’s in-memory data, and this is how the code is set up. The goal is to allow the user to filter the data and download the filtered version:

import dash_bootstrap_components as dbc
import pandas as pd
import dash
from dash_extensions.enrich import Input, Output, ServersideOutput, Dash, FileSystemStore
from dash_extensions import Download
from dash_extensions.snippets import send_data_frame
import flask

global_df = pd.read_csv('some_data.csv')

output_defaults = dict(backend=FileSystemStore(cache_dir='server_path_here/cache_dir'),
                       session_check=True)
server = flask.Flask(__name__)
app = Dash(__name__, output_defaults=output_defaults, server=server)

content = dbc.Card([
    dbc.CardHeader([
        dbc.Button('Download Filtered Data', color='success', block=True, id='download', n_clicks=0),
        Download(id='done-downloading'),
        dbc.Spinner(dcc.Store(id='intermediate-data'), fullscreen=True),
        ])
    ])

app.layout = dbc.Container([content], fluid=True)

@app.callback(ServersideOutput('intermediate-data', 'data'),
              [Input('filter1', 'value')])
def get_data(filter1):
    df = global_df.copy()
    if filter1:
        df = df[df['FILTERCOL'].isin(filter1)]
    return df

@app.callback(Output('done-downloading', 'data'),
              [Input('download', 'n_clicks'),
               Input('intermediate-data', 'data')])
def download_data(download, df):
    changed_id = [p['prop_id'] for p in dash.callback_context.triggered][0]
    if 'download' in changed_id:
        return send_data_frame(df.to_excel, 'filtered_data_download.xlsx')
    else:
        None
        
if __name__ == '__main__':
    server.run(host='0:0:0:0')

When your data is in-memory, there should be no difference between Heroku and local deployment. Do you see any errors in the JS console? Or in the Heroku log?

I’m deploying using a private Ubuntu server. No errors being shown anywhere that I could find… does this only work with Heroku?

No, it should work everywhere. I just mentioned Heroku as many people are using it :slight_smile:

Is there any way to show where the file is trying to, like somewhere I could add a print statement? I looked through the code for the Download component and send_data_frame and didn’t see a path anywhere (I admit, I don’t totally understand how it works!)… just wondering if there’s something I can do to troubleshoot :thinking: I’ve had permissions and path issues with other components (like the ServersideOutput), and specifying a path and adjusting the permissions for that folder has fixed those issues.

When you download in-memory data, it’s designed so that data never touches the disk (for speed, and to avoid issues with ephemeral file systems and/or permissions). My previous question was about ruling out problems with paths of the source files (but as the data is in-memory there aren’t any). The download itself (I.e. where the file ends up) should be handled by the browser. Have you tried printing the data just before it’s sent to the download component?