Dash DataTable - Downloading Selected Rows

I’m trying to incorporate this ‘Download Data’ into the dash_table_example. I have it working, however I can’t seem to figure out how to keep the ‘selected_row_indices’ aligned to what is visually shown on the dt.DataTable() element. Ie. I only want to download the selected rows, but can only do so right now based on the index of the original dataframe. I’d like to do it based on the updated dt.DataTable() - after filtering, sorting, etc.

I’m sure it is simple (and I’m a newbie) so just looking for someone to get me unstuck.

Here is my code:

import dash
from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html
import dash_table_experiments as dt
import json
import pandas as pd
import numpy as np
import plotly
import urllib
import flask
import io

app = dash.Dash()

app.scripts.config.serve_locally = True
# app.css.config.serve_locally = True

DF_WALMART = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/1962_2006_walmart_store_openings.csv')

DF_GAPMINDER = pd.read_csv(
    'https://raw.githubusercontent.com/plotly/datasets/master/gapminderDataFiveYear.csv'
)
DF_GAPMINDER = DF_GAPMINDER[DF_GAPMINDER['year'] == 2007]
DF_GAPMINDER.loc[0:20]

DF_SIMPLE = pd.DataFrame({
    'x': ['A', 'B', 'C', 'D', 'E', 'F'],
    'y': [4, 3, 1, 2, 3, 6],
    'z': ['a', 'b', 'c', 'a', 'b', 'c']
})

ROWS = [
    {'a': 'AA', 'b': 1},
    {'a': 'AB', 'b': 2},
    {'a': 'BB', 'b': 3},
    {'a': 'BC', 'b': 4},
    {'a': 'CC', 'b': 5},
    {'a': 'CD', 'b': 6}
]


app.layout = html.Div([
    html.H4('Gapminder DataTable'),
    dt.DataTable(
        rows=DF_GAPMINDER.to_dict('records'),

        # optional - sets the order of columns
        columns=sorted(DF_GAPMINDER.columns),

        row_selectable=True,
        filterable=True,
        sortable=True,
        selected_row_indices=[],
        id='datatable-gapminder'
    ),
    html.Div(id='selected-indexes'),
    html.A(
        'Download Data',
        id='download-link',
        download="rawdata.csv",
        href="",
        target="_blank"
    ),
    dcc.Graph(
        id='graph-gapminder'
    ),
], className="container")



def filter_data(selected_row_indices):
        dff = DF_GAPMINDER.iloc[selected_row_indices]
        return dff

@app.callback(
    dash.dependencies.Output('download-link', 'href'),
    [dash.dependencies.Input('datatable-gapminder', 'selected_row_indices')])
def update_download_link(selected_row_indices):
    dff = filter_data(selected_row_indices)
    csv_string = dff.to_csv(index=False, encoding='utf-8')
    csv_string = "data:text/csv;charset=utf-8," + urllib.parse.quote(csv_string)
    return csv_string



@app.callback(
    Output('datatable-gapminder', 'selected_row_indices'),
    [Input('graph-gapminder', 'clickData')],
    [State('datatable-gapminder', 'selected_row_indices')])
def update_selected_row_indices(clickData, selected_row_indices):
    if clickData:
        for point in clickData['points']:
            if point['pointNumber'] in selected_row_indices:
                selected_row_indices.remove(point['pointNumber'])
            else:
                selected_row_indices.append(point['pointNumber'])
    return selected_row_indices


@app.callback(
    Output('graph-gapminder', 'figure'),
    [Input('datatable-gapminder', 'rows'),
     Input('datatable-gapminder', 'selected_row_indices')])
def update_figure(rows, selected_row_indices):
    dff = pd.DataFrame(rows)
    fig = plotly.tools.make_subplots(
        rows=3, cols=1,
        subplot_titles=('Life Expectancy', 'GDP Per Capita', 'Population',),
        shared_xaxes=True)
    marker = {'color': ['#0074D9']*len(dff)}
    for i in (selected_row_indices or []):
        marker['color'][i] = '#FF851B'
    fig.append_trace({
        'x': dff['country'],
        'y': dff['lifeExp'],
        'type': 'bar',
        'marker': marker
    }, 1, 1)
    fig.append_trace({
        'x': dff['country'],
        'y': dff['gdpPercap'],
        'type': 'bar',
        'marker': marker
    }, 2, 1)
    fig.append_trace({
        'x': dff['country'],
        'y': dff['pop'],
        'type': 'bar',
        'marker': marker
    }, 3, 1)
    fig['layout']['showlegend'] = False
    fig['layout']['height'] = 800
    fig['layout']['margin'] = {
        'l': 40,
        'r': 10,
        't': 60,
        'b': 200
    }
    fig['layout']['yaxis3']['type'] = 'log'
    return fig


app.css.append_css({
    "external_url": "https://codepen.io/chriddyp/pen/bWLwgP.css"
})

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

To add a bit more context. I want to d/l selected rows from this example. If I filter or resort the dash.datatable, the indexing (selected_row_indices) is updated as it should. However, I don’t have a way to subset this from the original dataframe.

Is there anyway to convert the dash.datatable to a dataframe? That way I can recreate the data as shown in the dash.datatable and have the correct indexing.

The specific code with callback is below.

def filter_data(selected_row_indices):
        dff = DF_GAPMINDER.iloc[selected_row_indices]
        return dff

@app.callback(
    dash.dependencies.Output('download-link', 'href'),
    [dash.dependencies.Input('datatable-gapminder', 'selected_row_indices')])
def update_download_link(selected_row_indices):
    dff = filter_data(selected_row_indices)
    csv_string = dff.to_csv(index=False, encoding='utf-8')
    csv_string = "data:text/csv;charset=utf-8," + urllib.parse.quote(csv_string)
    return csv_string

any input on creating a Pandas DataTable() from the Dash DataTable()?

That would allow me to download the selected rows after resorting Dash DataTable().

@chriddyp?

Hi,
I think you are already there! Not sure if I miss anything but the best way for me would be to create a df from your currently displayed table rows and then do a simple df filtering based on your selected row indices, then save the df to CSV etc.

Quick try for callback would be:

@app.callback(Output('save-table-textbox', 'children'),
             [Input('save-table-button', 'n_clicks')],
             [State('table', 'rows'),
              State('table', 'selected_row_indices')]
              )

def save_current_table(savebutton, tablerows, selected_row_indices):

    table_df = pd.DataFrame(tablerows) #convert current rows into df

    if selected_row_indices:
        table_df = table_df.loc[selected_row_indices] #filter according to selected rows

    if savebutton:
        table_df.to_csv(filename)
        return "Current table saved."
1 Like

@jlbgit this is exactly what i was looking for! thank you!

1 Like

@jlbgit Thank you for great example. When I changed text value in the cell manually (for example changing country name Afghanistan to Afg , this change is omitted in the downloaded csv file. Is it possible to save DataTable after it is changed manually ? If yes would you share example please?

You could also try using the table’s derived_virtual_data attribute as a State in your callback. From the DataTable documentation (Reference | Dash for Python Documentation | Plotly), this is:

This property represents the visible state of data across all pages after the front-end sorting and filtering as been applied.