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

Create new closable buttons once a button is pressed

Hi everyone,
I’m trying to create a dashboard that after selecting a city from the dropdown options, and then pressing the ADD A CITY, the following behavior should be expected:

  • a new closable button appears next to the dropdown menu, and
  • the corresponding plot should appear in the figure.

What I want is something similar to the following sketch

The code is given below

import pandas as pd
import numpy as np

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Output, Input
import plotly.graph_objs as go

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

# Generate data
cities = ["Montreal", "Toronto", "Vancouver"]
t = np.linspace(start=0, stop=20, num=100)
data_df = pd.DataFrame({
    "t": t,
    cities[0]: np.sin(t),
    cities[1]: np.cos(t),
    cities[2]: np.sin(t + 2),
})

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

# HTML layout
app.layout = html.Div(children=[
    dcc.Location(id="initialization", refresh=False),
    html.Div(
        [
            html.Div([dcc.Dropdown(
                id="city-dropdown",
                placeholder="Select a city",
                multi=False,
            )], style={"width": "20%", "display": "inline-block"}),
            html.Div([html.Button("Add a city", id="add-city-button", value="testing", style={"width": "20%", "display": "inline-block"})]),
        ],
    ),
    html.Div(dcc.Graph(id="ts-plot"))
])


# Callbacks
@app.callback(
    Output("city-dropdown", "options"),
    [Input("initialization", "search")]
)
def available_cities(unused_string):
    return [{"label": city, "value": city} for city in cities]


@app.callback(
    Output("ts-plot", "figure"),
    [
        Input("city-dropdown", "value"),
        Input("add-city-button", "n_clicks")
    ]
)
def update_figure(selected_city, n_clicks):
    if selected_city is None:
        data = []
    else:
        data = [go.Scatter(
            x=data_df["t"],
            y=data_df[selected_city],
            mode="lines",
            line={"dash": "dash"},
            name="testing"
        )]
    return {
        "data": data,
        "layout": dict(
            xaxis={"title": "time", "range": [data_df["t"].min(), data_df["t"].max()]},
            yaxis={"title": "", "range": [-2, 2]},
            hovermode="closest"
        ),
    }


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

currrently, the dashboard looks like the following:

How about using a dcc.Dropdown with multi=True instead?

dcc.Dropdown(
  id='cities',
  multi=True,
  options=[{'label':'Montreal','value':'Montreal'},
                {'label' : 'Toronto', 'value':'Toronto'}],
  value="empty if you dont want any pre selected"
)

Value is a list of your selected options.

@RenaudLN @seferoezcan I’m aware of that option, but I want separate buttons

So, far, I was successful in creating buttons. However, the problem is that once the buttons are created, I cannot delete (close) them.

import pandas as pd
import numpy as np

import dash
from dash.dependencies import Output, Input, State
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objs as go

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

selected_cities = set()

# Generate data
cities = ["Montreal", "Toronto", "Vancouver"]
t = np.linspace(start=0, stop=20, num=100)
data_df = pd.DataFrame({
    "t": t,
    cities[0]: np.sin(t),
    cities[1]: np.cos(t),
    cities[2]: np.sin(t + 2),
})
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.config['suppress_callback_exceptions']=True


# HTML layout
app.layout = html.Div(children=[
    dcc.Location(id="initialization", refresh=False),
    html.Div(
        [
            html.Div([dcc.Dropdown(
                id="city-dropdown",
                placeholder="Select a city",
                multi=False,
            )], style={"width": "20%", "display": "inline-block"}),
            html.Div([html.Button("Add a city", id="add-city-button", style={"width": "20%", "display": "inline-block"})]),
            html.Div(id="selected-city-buttons"),
            # html.Div(id="selected-city-buttons2"),
            # Hidden div inside the app that stores the intermediate value
            html.Div(id="intermediate-set-cities", style={"display": "none"}),
            html.Div(id="triggered-button-id"),
            html.Div(id="intermediate-set-cities2", style={"display": "none"}),

        ],
    ),
    html.Div(dcc.Graph(id="ts-plot"))
])


# Callbacks
@app.callback(
    Output("city-dropdown", "options"),
    [Input("initialization", "search")]
)
def available_cities(unused_string):
    return [{"label": city, "value": city} for city in cities]


@app.callback(
    [
        Output("selected-city-buttons", "children"),
        Output("intermediate-set-cities", "value"),
    ],
    [Input("add-city-button", "n_clicks")],
    [State("city-dropdown", "value")],
)
def update_buttons(add_n_clicks, city):
    # available_cities = []
    buttons = []
    button_ids = []
    if city is not None:
        selected_cities.add(city)

    for item in selected_cities:

        buttons.append(
            html.Button(f"Button_{item}", id=f"button_{item}")
        )

    print(buttons)
    print(add_n_clicks)
    print(selected_cities)
    print("button_ids", button_ids)
    return buttons, list(selected_cities)


@app.callback(
    Output("ts-plot", "figure"),
    [
        Input("intermediate-set-cities", "value"),
    ]
)
def update_figure(cities):
    print(f"cities: {cities}")
    data = []
    if cities is None:
        data = []
    else:
        for city in cities:
            data.append(go.Scatter(
                x=data_df["t"],
                y=data_df[city],
                mode="lines",
            ))

    return {
        "data": data,
        "layout": dict(
            xaxis={"title": "time", "range": [data_df["t"].min(), data_df["t"].max()]},
            yaxis={"title": "", "range": [-2, 2]},
            hovermode="closest"
        ),
    }
if __name__ == '__main__':
    app.run_server(debug=True)

The output looks like the following:

So, I want the buttons Button_TORONTO and/or BUTTON_MONTREAL to disappear when clicking on them. And, also the corresponding plot should disappear as well. The problem here is that the button component_id is not defined until we select a city from the dropdown and then press on the ADD A CITY.

Hi @essi1990, can you tell us a little more the point of adding these buttons via a press on yet another button? This seems to make user experience more complex.

In dash you have to declare all your callbacks beforehand so you could create the callbacks for each “city button” to trigger plot and button display update on clicks. But once again I would not do this and rather use the multi dropdown. You can add some styling to make the selected dropdown items look like buttons. I’ll try to post a snippet later to show this.

Here we go

First app.py

import pandas as pd
import numpy as np

import dash
from dash.dependencies import Output, Input, State
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objs as go

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

selected_cities = set()

# Generate data
cities = ["Montreal", "Toronto", "Vancouver"]
t = np.linspace(start=0, stop=20, num=100)
data_df = pd.DataFrame({
    "t": t,
    cities[0]: np.sin(t),
    cities[1]: np.cos(t),
    cities[2]: np.sin(t + 2),
})
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.config['suppress_callback_exceptions']=True


# HTML layout
app.layout = html.Div(children=[
    dcc.Location(id="initialization", refresh=False),
    html.Div(
        [
            html.Div("Cities: ", id="city-label"),
            html.Div([dcc.Dropdown(
                id="city-dropdown",
                placeholder="Select one or several cities",
                multi=True,
                value=[],
                clearable=False,
            )], className="multi-button-dropdown"),
        ], id="controls-container"
    ),
    html.Div(dcc.Graph(id="ts-plot"))
])


# Callbacks
@app.callback(
    Output("city-dropdown", "options"),
    [Input("initialization", "search")]
)
def available_cities(unused_string):
    return [{"label": city, "value": city} for city in cities]


@app.callback(
    Output("ts-plot", "figure"),
    [
        Input("city-dropdown", "value"),
    ]
)
def update_figure(cities):
    data = []
    if cities:
        for city in cities:
            data.append(go.Scatter(
                x=data_df["t"],
                y=data_df[city],
                mode="lines",
                name=city
            ))

    return {
        "data": data,
        "layout": dict(
            xaxis={"title": "time", "range": [data_df["t"].min(), data_df["t"].max()]},
            yaxis={"title": "", "range": [-2, 2]},
            hovermode="closest"
        ),
    }
if __name__ == '__main__':
    app.run_server(debug=True)

And assets/style.css

#controls-container {
    display: flex;
}

#city-label {
    flex: 0 0 auto;
    height: 36px;
    line-height: 36px;
    margin-right: 20px;
}

.multi-button-dropdown {
    flex: 0 0 auto;
}

.multi-button-dropdown .Select-control, .multi-button-dropdown .Select-menu-outer {
    border: none !important;
    min-width: 250px;
    width: auto;
    border-radius: 0 !important;
    box-shadow: none !important;
}

.multi-button-dropdown .Select-control:hover {
    background-color: rgba(243, 243, 243, 1);
}

.multi-button-dropdown .Select-value {
    position: relative;
    border: none;
}

.multi-button-dropdown .Select-value-icon {
    opacity: 0;
    width: 100%;
    position: absolute;
}

.multi-button-dropdown .Select-value-label {
    background-color: rgb(72,72,72);
    color: rgb(255, 255, 255);
    border: none;
    border-radius: 3px;
    height: 24px;
    line-height: 1.1;
    padding: 5px 15px;
    box-sizing: border-box;
}

2 Likes

Hi @RenaudLN. I know the user experience would be more complex. I personally would have preferred the multi-options in the drop-down. However, I got the design from a web designer, and I did not have the flexibility to change the button design structure!

I really like your implementation, especially with the css styling.

My current implementation involves creating buttons and updating the button styles based on the selected option (in this case, city).

Thanks a lot! Your implementation actually helped me a lot with the styling and the the interaction. :pray::pray: