Robert Allan
Robert Allan

Reputation: 115

Python Dash Datatable Input

I am new to Python Dash, so I am not sure what is possible. What I would like to accomplish is making a datatable where the user is able to manipulate data in the datatable which would result in changes for other graphs.

Here is an example of what I am trying to do:

I have information about people’s grocery lists and I would like to be able to change what is in their list

The data that I have is structured as such

Name Item Amount
Bob Apple 1
Bob Banana 2
Anna Apple 2
Anna Banana 1

I would like to be able to have a datatable where there is a drop down to select the person’s list so the datatable would just have their items and amounts. In the selected datatable, they should be able to add and delete rows, change the item with a dropdown in the cell, and change the amount by typing in the desired amount.

I have been able to create this somewhat where you can select a person’s list, and the item can be changed based on a list of foods given, but I cannot figure out how to do the rest. Here is what I have been able to put together for my example

from dash import dash_table as dt
from dash import dcc
from dash import html
from dash.dependencies import Input
from dash.dependencies import Output
import pandas as pd

df = pd.DataFrame({'Name' : ['Bob', 'Bob', 'Anna', 'Anna'],
                   'Item' : ['Apple', 'Banana', 'Apple', 'Banana'],
                   'Amount' : [1, 2, 2, 1]})

app = dash.Dash(__name__)

names = df['Name'].unique().tolist()
app.layout = html.Div(
    children=[
        dcc.Dropdown(
            id="filter_dropdown",
            options=[{"label": name, "value": name} for name in names],
            placeholder="-Select a Person-",
            multi=False,
            value=df['Name'].values,
        ),
        dt.DataTable(
            id="table-container",
            columns=[{'name': 'Item', 'id': 'Item', 'presentation':'dropdown'},
                     {'name': 'Amount', 'id': 'Amount'}],
            data=df.to_dict("records"),
            editable=True,
            dropdown={
                'Item' : {
                    'options': [
                        {'label': i, 'value': i}
                        for i in list(df['Item'].unique())
                        ]
                    }
                }
        )
    ]
)
@app.callback(
    Output("table-container", "data"), 
    Input("filter_dropdown", "value"))
def display_table(name):
    dff = df[df['Name'].isin([name])]
    return dff.to_dict("records")
    

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

Here is what it looks like in the browser enter image description here

I can select an item from the dropdown in the cell and it will appear to change it, but when I switch between people it loses its change that I made. I also cannot change the amounts. The next step would be is if I can use all the data as well as the changes in the data in another graph (for example to show how many people have a certain item). If someone would be able to help, I would appreciate it.

Thanks

Upvotes: 2

Views: 3049

Answers (2)

John Collins
John Collins

Reputation: 2951

Here is another possible solution/approach.

Use dash.dcc.Store to save different "shoppers"' grocery lists in browser memory storage cache

import pandas as pd

from collections import OrderedDict

from dash import Dash, Input, Output, State, dcc, html, callback
from dash.exceptions import PreventUpdate
from dash import dash_table


names = ["Anna", "Bob", "John", "Robert", "Sarah"]
shoppers_df = pd.DataFrame(
    OrderedDict(
        [
            ("Name", names),
            ("Item", [""] * len(names)),
            ("Amount", [""] * len(names)),
        ]
    )
).set_index("Name")

empty_shopping_list = (
    pd.DataFrame(columns=["Name", "Item", "Amount"])
    .set_index("Name")
    .to_dict("records")
)

items = [
    "Apple",
    "Avocados",
    "Baking powder",
    "Balsamic vinegar",
    "Banana",
    "Breadcrumbs",
    "Carrots",
    "Chicken",
    "Chili Powder",
    "Cilantro",
    "Crushed Red Pepper",
    "Diced Tomatoes",
    "Eggs",
    "Ground Beef",
    "Juice Concentrate",
    "Milk",
    "Parsley",
    "Pizza or Other Convenience Meals",
    "Sliced Turkey",
    "Tortillas",
    "Vegetable/Canola Oil",
    "White Vinegar",
]


app = Dash(__name__)


app.layout = html.Div(
    [
        dcc.Store(id="session", storage_type="session"),
        html.Div(
            html.H1("Multi-Shopper Dynamic Grocery Lists"),
            style={"text-align": "center"},
        ),
        html.Hr(),
        html.H2("Shopper", style={"text-align": "center"}),
        dcc.Dropdown(
            id="filter-dropdown",
            options=[{"label": name, "value": name} for name in names],
            placeholder="-Select a Person-",
            multi=False,
            value=None,
        ),
        html.H3("Add new item to list:", style={"text-align": "center"}),
        html.Div(
            [
                dcc.Input(id="add-items-num", type="number",),
                html.Button("Add", id="add-items", n_clicks=0),
            ],
            style={"text-align": "center"},
        ),
        dash_table.DataTable(
            id="table",
            columns=[
                {"id": "Item", "name": "Item", "presentation": "dropdown"},
                {"id": "Amount", "name": "Amount", "type": "numeric"},
            ],
            data=empty_shopping_list,
            editable=True,
            dropdown_conditional=[
                {
                    "if": {"column_id": "Item",},
                    "options": [{"label": i, "value": i} for i in items],
                },
            ],
            style_cell={
                "fontSize": "0.8rem",
                "whiteSpace": "normal",
                "padding": "3px",
                "textAlign": "center",
                "maxWidth": "300px",
            },
            style_header={
                "fontWeight": "500",
                "fontSize": "0.8rem",
                "cursor": "pointer",
            },
        ),
        html.Div(id="table-container"),
        html.Br(),
        html.Div(
            [
                html.Button(
                    "Save", id="submit-save", style={"text-align": "center"}
                )
            ],
            style={"text-align": "center"},
        ),
    ]
)


@callback(
    Output("table", "data", allow_duplicate=True),
    Output("session", "data"),
    Input("filter-dropdown", "value"),
    Input("submit-save", "n_clicks_timestamp"),
    State("table", "data"),
    State("table", "columns"),
    State("session", "data"),
    prevent_initial_call=True,
)
def display_table(name, save, rows, columns, session):
    if not session:
        print("Initializing browser storage session...")
        session = {"current_shopper": name, "last_save": 0}
    if not name:
        session["current_shopper"] = "None"
        return [empty_shopping_list, session]

    df = pd.DataFrame(rows, columns=[c["name"] for c in columns])

    if save:
        if save > session["last_save"]:
            print(f"Saving: {save}")
            session[name] = df.to_dict()
            session["last_save"] = save

    if session["current_shopper"] != name:
        session["current_shopper"] = name

    if name in session.keys():
        df = pd.DataFrame.from_dict(session[name])
    else:
        if len(rows) < 1:
            print(f"Creating new shopping list table for {name}")
            df = pd.DataFrame(shoppers_df.loc[name]).T
        else:
            df = pd.concat([df, shoppers_df.loc[name]])

    return [df.to_dict("records"), session]


@callback(
    Output("table", "data", allow_duplicate=True),
    Input("add-items", "n_clicks"),
    State("add-items-num", "value"),
    State("table", "data"),
    State("table", "columns"),
    prevent_initial_call=True,
)
def add_rows(n_clicks, n_new_rows, rows, columns):
    if n_clicks > 0:
        df = pd.DataFrame(rows, columns=[c["name"] for c in columns])
        df = pd.concat(
            [
                df,
                pd.DataFrame(
                    [["", ""]] * n_new_rows, columns=["Item", "Amount"]
                ),
            ]
        )
        return df.to_dict("records")
    else:
        raise PreventUpdate


@callback(
    Output("table", "data"),
    Input("table", "data_timestamp"),
    State("table", "data"),
    State("table", "columns"),
    State("session", "data"),
)
def update_table(timestamp, rows, columns, session):
    df = pd.DataFrame(rows, columns=[c["name"] for c in columns])
    return df.to_dict("records")


if __name__ == "__main__":
    app.run(debug=True)

Dash App Demo – Example

Python Plotly Dash web app for creating dynamic multi-user grocery lists:

  • with preservation of previous manual edits to dash_table,
  • and built-in dropdown components to select from list of possible grocery items

live video gif of demo app

Upvotes: 0

Robert Allan
Robert Allan

Reputation: 115

After doing some work, I do have it working. It is by means not the optimal or right way to do this probably, but it does what I want it to do. I ended up using a global variable for my data frame and whenever there was a change to the data table, I would filter out the data from the data frame and add the modified data from the data table.

To get a visual to work, I listen for changes to the data table. I know that there is probably a race between updating the global variable and returning the new visual, but I did not have to delay the visual being updated.

Below is the code that I came up with. I hope this helps others somehow if they wanted to make a simple "spreadsheet" that people can manipulate and result in a responsive visual related to the data.

import dash
import pandas as pd
from dash import dash_table as dt
from dash import dcc
from dash import html
from dash.dependencies import Input
from dash.dependencies import Output
import plotly.express as px

df = pd.DataFrame({'Name' : ['Bob', 'Bob', 'Anna', 'Anna'],
                   'Item' : ['Apple', 'Banana', 'Apple', 'Banana'],
                   'Amount' : [1, 2, 2, 1]})

app = dash.Dash(__name__)

names = df['Name'].unique().tolist()
app.layout = html.Div(
    children=[
        dcc.Dropdown(
            id="filter_dropdown",
            options=[{"label": name, "value": name} for name in names],
            placeholder="-Select a Person-",
            multi=False,
            value=df['Name'].values,
        ),
        dt.DataTable(
            id="table-container",
            columns=[{'name': 'Item', 'id': 'Item', 'presentation':'dropdown'},
                     {'name': 'Amount', 'id': 'Amount'}],
            data=df.to_dict("records"),
            editable=True,
            row_deletable=True,
            dropdown={
                'Item' : {
                    'options': [
                        {'label': i, 'value': i}
                        for i in list(df['Item'].unique())
                        ]
                    }
                }
        ),
        html.Button('Add', id='add_btn', n_clicks = 0),
        dcc.Graph(id='visual')
    ]
)
@app.callback(
    Output("table-container", "data"), 
    Input("filter_dropdown", "value"),
    Input("table-container", "data"),
    Input("table-container", "columns"),
    Input("add_btn", 'n_clicks'))
def display_table(name, rows, columns, n_clicks):
    e = dash.callback_context.triggered[0]['prop_id']
    global df
    if e in ["table-container.data", "table-container.columns"]:
        temp = pd.DataFrame(rows, columns=[c['name'] for c in columns])
        temp['Name'] = name
        df = df[~df['Name'].isin([name])]
        df = df.append(temp)
    elif e == 'add_btn.n_clicks':
        if n_clicks > 0:
            df = df.append(pd.DataFrame({'Name':name, 'Item': '', 'Amount': 0}, index=[0]))
    dff = df[df['Name'].isin([name])].to_dict("records")
    return dff
    
@app.callback(Output('visual', 'figure'),
              Input("table-container", "data"),
              Input("table-container", "columns"))
def display_graph(rows, columns):
    fig = px.bar(df, x='Item', y='Amount')
    return fig

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

Upvotes: 1

Related Questions