Harxish
Harxish

Reputation: 459

Dash DropDown closes after click

I don't want my dropdown to close after choosing a value, I want to it to stay opened on my page. I'm using dcc.Dropdown.

dcc.Dropdown(id='job-type', options=self.options, 
             placeholder='Select one or more Event(s)',
             value=self.job_type, multi=True)

Upvotes: 2

Views: 2041

Answers (3)

Atul
Atul

Reputation: 71

I found this alternative dash library which does not auto close after multi selection

https://www.dash-mantine-components.com/components/multiselect

For those still looking for this feature, I thought it is worth mentioning!

Upvotes: 1

John Collins
John Collins

Reputation: 2961

Solution #2

How about instead another possible approach, a dcc.Checklist paired with an html.Summary (which leverages built-in collapsibility, thus, mimicking a dropdown menu) within an html.Details component? This much more closely satisfies what you are asking for - a kind of dropdown menu which wont automatically close itself after each selection of any of its listed options.

E.g.,

Mock Data

A local file called "jobs.csv", tab-delimited, with the following contents:

code    options job_type
13-2011.00  Accountants and Auditors        Business and Financial Operations
27-2011.00  Actors  Arts, Design, Entertainment, Sports, and Media
15-2011.00  Actuaries   Computer and Mathematical
29-1291.00  Acupuncturists  Healthcare Practitioners and Technical
55-1011.00  Air Crew Officers   Military Specific
23-1022.00  Arbitrators, Mediators, and Conciliators    Legal
17-1011.00  Architects, Except Landscape and Naval  Architecture and Engineering
19-2011.00  Astronomers Life, Physical, and Social Science
33-3011.00  Bailiffs    Protective Service
51-3011.00  Bakers  Production
39-5011.00  Barbers Personal Care and Service
15-2099.01  Bioinformatics Technicians  Computer and Mathematical
25-1042.00  Biological Science Teachers, Postsecondary  Educational Instruction and Library
19-1029.00  Biological Scientists, All Other    Life, Physical, and Social Science
19-4021.00  Biological Technicians  Life, Physical, and Social Science
19-1029.04  Biologists  Life, Physical, and Social Science
51-8013.03  Biomass Plant Technicians   Production
11-3051.04  Biomass Power Plant Managers    Management
15-2041.01  Biostatisticians    Computer and Mathematical
15-1299.07  Blockchain Engineers    Computer and Mathematical
47-2011.00  Boilermakers    Construction and Extraction

Quasi-"Dropdown" Component

In layout.py:

children = [
html.Details(
    [
        html.Div(
            [
                dcc.Checklist(
                    id="jobs-multi-dropdown",
                    options=[
                        {"label": f"{job_title}", "value": f"{job_type}"}
                        for (job_title, job_type) in zip(
                            df_jobs.options, df_jobs.job_type
                        )
                    ],
                )
            ],
            className="updates-list",
        ),
        html.Summary(
            html.Code(f"✔ JOBS"),
            style={"color": "rgb(24, 230, 112)"},
            className="updates-header",
        ),
    ],
    id="jobs-selection",
),
html.Br(),
html.Br(),
html.Div(
    [html.Button("Submit", id="jobs-selected", n_clicks=0)],
    style={"display": "flow-root"},
),
html.Br(),
html.H3("Job Types Selected:"),
html.Code(id="job-type"),
html.Br(),
]

In callbacks.py:

@app.callback(
    Output("job-type", "children"),
    [Input("jobs-selected", "n_clicks"), State("jobs-multi-dropdown", "value")],
)
def choose_job(n_click, job_types):
    """ Returns interactively the associated job "type"
    """
    if job_types:
        return [f"{n} {job_type}, " for (n, job_type) in enumerate(job_types)]
    else:
        return ["Select any number of jobs from the list above."]

Before any selections

↓ Then, after clicking the jobs component, it expands, to a scrollable dropdown component which is actually literally a dcc.checklist:

Scrollable dropdown

↓ Then, after clicking the submit button, the corresponding types appear:

After clicking submit

To close the "dropdown", you simply re-click the "✅ Jobs" rounded component, which is the html.Summary dash component. And the "details" are the dcc.Checklist.*

*Usually there is by default an arrow, or triangle rather, symbol which "twists" (I guess apparently is the common term: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details) which helps to signal to the user that you can click on this to expand it. For some reason in my screenshots it has been made invisible but just copying the code I provide, it should show up.

In assets/custom.css:

@import url('https://fonts.googleapis.com/css?family=Cinzel:300,400,500,600,700|Muli:200,300,400,500,600|Open+Sans:200,300,400,500,600|Oswald:200,300,400,500,600&subset=latin-ext');
@import url('https://fonts.googleapis.com/css?family=Montserrat:200,200i,300,300i,400,400i,500,500i');
@import url('https://fonts.googleapis.com/css?family=Raleway:300,400,500,600,800&subset=latin-ext');
@import url('https://fonts.googleapis.com/css?family=Roboto+Mono:200,300,400,500');
@keyframes glow{0%{text-shadow:0 0 5px rgba(255,255,255,.5),0 0 5px rgba(255,255,255,.5),0 0 5px rgba(3,242,255,.5),0 0 30px rgba(0,230,128,0.4706),0 0 5px rgba(255,235,59,.5),0 0 5px rgba(0,0,255,.5),0 0 15px rgba(3,242,255,.5)}50%{text-shadow:0 0 5px rgba(0,0,255,.75),0 0 5px rgba(238,130,238,.75),0 0 5px rgba(187,77,255,0.549),0 0 30px rgba(77,255,192,.75),0 0 5px rgba(255,235,59,.75),0 0 5px rgba(128,0,128,.75),0 0 15px rgba(187,77,255,0.549)}75%{text-shadow:0 0 5px rgba(255,165,0,.25),0 0 5px rgba(255,165,0,.25),0 0 5px rgba(230,0,115,.25),0 0 30px rgba(230,0,115,.25),0 0 5px rgba(255,235,59,.25),0 0 5px rgba(255,0,0,.25),0 0 15px rgba(230,0,115,.25)}100%{text-shadow:0 0 20px rgba(127,255,0,.5),0 0 20px rgba(0,255,0,.5),0 0 10px rgba(255,255,0,.5),0 0 20px rgba(255,193,7,.5),0 0 10px rgba(255,255,0,.5),0 0 20px rgba(255,215,0,.5),0 0 20px rgba(77,255,192,.5)}}

h1 { font-size: 3.5rem; font-family: 'Montserrat'; text-rendering: optimizeLegibility; color: #0d04a5; font-weight: 500; text-decoration: none; border-bottom: 0.0px solid gray; line-height: 4rem; text-decoration: underline }
h2 { font-family: 'Oswald', serif; color: var(--pph-color-8); cursor: default; font-weight: 300; font-size: 2rem; }
h3 { font-size: 2.0rem; font-family: 'Montserrat', sans-serif; font-weight: 300; color: rgb(32, 92, 188); cursor: default }
h4 { font-size: 1.5rem; font-family: 'Oswald', sans-serif; color: var(--pph-color-57); font-weight: 400; cursor: default }
h5 { font-size: 1.2rem; font-family: 'Muli', sans-serif; cursor: default }
h6 { font-size: 1.1rem; color: #333; font-weight: 400 }
@media (min-width:550px) input[type="checkbox"], input[type="radio"] {
    details#jobs-selection { display: inline-block; width: 80%; }
    ol.updates-list > li { margin-bottom: 5px; padding-left: 3%; margin-left: 8%; margin-right: 5%; list-style-type: auto; list-style-position: outside }
    summary { cursor: pointer }
text-rendering:optimizeLegibility; -moz-appearance: none; display: inline-block; background-color: #f1f1f1; color: #666; top: 10px; height: 30px; width: 30px; border: 0; border-radius: 50px; cursor: pointer; margin-right: 7px; outline: none; }
.updates-list { font-family: 'Roboto Mono'; font-size: .75rem; color: #064d56f0; text-align: left; width: 30%; margin-left: 35%; padding: 10px; padding-bottom: 24px; border: 1px solid #3f51b54d; box-sizing: border-box; box-shadow: 0px 10px 25px -12px black; max-height: 400px; overflow: auto }
.updates-header:hover { animation: glow 2.5s infinite cubic-bezier(0.38, 0.39, 0.5, 0.51); }
.updates-header { font-weight: 500; border: 1px solid rgba(0, 0, 200, .33); width: 32%; border-radius: 30px; margin-left: 34%; box-sizing: border-box; display: block; text-align: center; margin-bottom: -2px; background-color: #00000085; letter-spacing: 8px; box-shadow: 0px 0px 8px -1px #00ff55ab; padding: 12px; padding-right: 2px; }
@media (min-width:550px)
.button:hover, button:hover, input[type="submit"]:hover, input[type="reset"]:hover, input[type="button"]:hover {
border-color: #00FFC050; background: #00000075; }
@media (min-width:550px)
.button, button, input[type="submit"], input[type="reset"], input[type="button"] {
display: inline-block; padding: 0 25px; color: #000000; text-align: center; font-size: 14px; font-weight: 500; font-family: "Cinzel", serif !important; line-height: 32px; text-decoration: none; white-space: nowrap; background-color: #ffffffcc !important; border-radius: 30px; border: 1px ridge #00000050; cursor: pointer; box-sizing: border-box; }


ORIGINAL SUGGESTION

(included just for reference, and, to also still show what's possible using the dcc.Dropdown component and the slightly different UX which can be implemented that way - it does have the advantage of being searchable and clearable)

Yes, there is indeed a specific dcc.Dropdown parameter "multi" which can be set to boolean True, and that should work to allow your users to select multiple options from the dropdown.

EDIT: Searching is enabled as default, so it's pretty fast and convenient to simply click the dropdown bar once to extend its options, either scroll and select with another mouse click (and then yes unfortunately an additional mouse click [by default behavior] is required to re-expand the list of options]) or, user can just begin typing the first letters of each option they want, and they will appear highlighted. So, typing text also re-expands the dropdown list. You can just press enter to add any highlighted option from the dropdown, and then continue typing for the next selection because the cursor's focus will have remained in the dropdown component text search field. It may be possible to hack/override the default CSS/JS behavior of the menu automatically closing after each selection, but it may be a bit tricky. I could try and help figure that out with you if that's what you really think is your necessary desired functionality for your UX.

In your layout file:

html.Br(),
html.H2("Jobs"),
dcc.Dropdown(
    id="jobs-multi-dropdown",
    value=None,
    clearable=True,
    optionHeight=50,
    multi=True,
    options=[
        {"label": f"{job_title}", "value": f"{job_type}"}
        for (job_title, job_type) in zip(df_jobs.options, df_jobs.job_type)
    ],
    placeholder="—🔍⤑Search all Jobs—",
),
html.Div(
    [html.Button("Submit", id="jobs-selected", n_clicks=0)],
    style={"display": "flow-root"},
),
html.Br(),
html.H3("Job Types Selected:"),
html.Code(id="job-type"),
html.Br(),
  

I am not sure exactly what you want done with the "type" information, but I created a callback triggered by a "submit" button and which also takes as State-type input the current value(s) selected from the dropdown, just to demonstrate.

You could add something like this to your callbacks.py file:

@app.callback(
    Output("job-type", "children"),
    [Input("jobs-selected", "n_clicks"), State("jobs-multi-dropdown", "value")],
)
def choose_job(n_click, job_types):
    """ Returns interactively the associated job "type"
    """
    if job_types:
        return [f"{n} {job_type}, " for (n, job_type) in enumerate(job_types)]
    else:
        return ["Select any number of jobs from the list above."]

which results in:

Dropdown as it first appears

↓ User can search, delete previous selections, and also even clear all selected at once using the little "x"'s

Notice: As any item becomes selected, it is automatically removed from the dropdown remaining options. -

Making multiple selections

↓ Then, after clicking the submit button, the corresponding types appear:

Showing the job types

Extra

And if you're curious here's some of the CSS, I am not sure this alone would work but it may help introduce you to the customizability available in Dash if you aren't already (this would go in a .css file located under a folder called "assets" & Dash will automatically find it and override its defaults with your own customizations):

@import url('https://fonts.googleapis.com/css?family=Cinzel:300,400,500,600,700|Muli:200,300,400,500,600|Open+Sans:200,300,400,500,600|Oswald:200,300,400,500,600&subset=latin-ext');
@import url('https://fonts.googleapis.com/css?family=Montserrat:200,200i,300,300i,400,400i,500,500i');
@import url('https://fonts.googleapis.com/css?family=Raleway:300,400,500,600,800&subset=latin-ext');
@import url('https://fonts.googleapis.com/css?family=Roboto+Mono:200,300,400,500');

h1 {
    font-size: 3.5rem;
    font-family: 'Montserrat', sans serif;
    text-rendering: optimizeLegibility;
    color: #0d04a5;
    font-weight: 500;
    text-decoration: none;
    border-bottom: 0.0px solid gray;
    line-height: 4rem;
    text-decoration: underline
}

h2 {
    font-family: 'Oswald', serif;
    color: #0a7fc2;
    cursor: default;
    font-weight: 300;
    font-size: 2rem;
}

h3 {
    font-size: 2.0rem;
    font-family: 'Montserrat', sans-serif;
    font-weight: 300;
    color: rgb(32, 92, 188);
    cursor: default
}

h4 {
    font-size: 1.5rem;
    font-family: 'Oswald', sans-serif;
    color: #1fadac;
    font-weight: 400;
    cursor: default
}

h5 {
    font-size: 1.2rem;
    font-family: 'Muli', sans-serif;
    cursor: default
}

h6 {
    font-size: 1.1rem;
    color: #333;
    font-weight: 400
}

.is-focused .Select-input>input {
    background-color: rgba(66, 66, 66, 0.46) !important;
    color: #46ffbb;
    margin-bottom: 1px;
    mix-blend-mode: hard-light;
}

.is-focused:not(.is-open)>.Select-control {
    cursor: pointer !important;
    border-color: rgba(10, 80, 250, 0.85);
    color: #0F00C6;
    box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 0 3px #46FFBB;
}

.is-open .Select-arrow, .Select-arrow-zone:hover>.Select-arrow {
    border-top-color: #666;
}

.is-open>.Select-control .Select-arrow {
    top: -2px;
    border-color: transparent transparent #999;
    border-width: 0 5px 5px
}

.is-open>.Select-control {
    border-color: #46ffbb #46ffefc7 #46ff6cd4 !important;
    border-radius: 5px !important;
    border-width: 3px;
}

.is-searchable.is-focused:not(.is-open)>.Select-control {
    cursor: text !important
}

.is-searchable.is-open>.Select-control {
    cursor: pointer !important;
    background: rgba(255, 255, 255, 0.18) !important;
}

.button, button, input[type="submit"], input[type="reset"], input[type="button"] {
    display: inline-block;
    padding: 0 25px;
    color: #000000;
    text-align: center;
    font-size: 14px;
    font-weight: 500;
    font-family: "Cinzel", serif !important;
    line-height: 32px;
    text-decoration: none;
    white-space: nowrap;
    background-color: #ffffffcc !important;
    border-radius: 30px;
    border: 1px ridge #00000050;
    cursor: pointer;
    box-sizing: border-box
}

.button.button-primary, button.button-primary, input[type="submit"].button-primary, input[type="reset"].button-primary, input[type="button"].button-primary {
    color: #00000075 !important;
    background-color: #33C3F050;
    border-color: #33C3F0
}

.button.button-primary:hover, button.button-primary:hover, input[type="submit"].button-primary:hover, input[type="reset"].button-primary:hover, input[type="button"].button-primary:hover, .button.button-primary:focus, button.button-primary:focus, input[type="submit"].button-primary:focus, input[type="reset"].button-primary:focus, input[type="button"].button-primary:focus {
    color: #00000075 !important;
    background-color: transparent;
    border-color: #1EAEDB
}

.button:focus, button:focus, input[type="submit"]:focus, input[type="reset"]:focus, input[type="button"]:focus {
    color: #5C5D86;
    border-color: #00000075 !important
}

.button:hover, button:hover, input[type="submit"]:hover, input[type="reset"]:hover, input[type="button"]:hover {
    border-color: #00FFC050;
    background: #00000075
}

.Select.is-clearable.is-searchable.Select--multi {
    width: 70 %;
    display: inline-block;
    margin-left: 15%;
}

.Select-placeholder {
    margin-left: -12%;
    background: transparent !important;
}

Upvotes: 1

vestland
vestland

Reputation: 61214

Based on:

I don't want my dropdown to close after choosing a value, I want to it to stay opened on my page.

... it seems to me that you're actually looking for the features and functionalities of a dcc.Checklist:

import dash_core_components as dcc
dcc.Checklist(
    options=[
        {'label': 'New York City', 'value': 'NYC'},
        {'label': 'Montréal', 'value': 'MTL'},
        {'label': 'San Francisco', 'value': 'SF'}
    ],
    value=['NYC', 'MTL']
)

Which in this case would produce:

enter image description here

And this would do exactly what you're describing with regards to functionality:

  1. You can select all, some or none of the options
  2. The checklist won't collapse when you make a selection.

Here's an example that uses the built-in px.stocks dataset:

enter image description here

This particular example will only replicate the already existing functionalities of the figure legend, but this setup should work as a good starting point if you'd like to free yourself of the limitations of using the legend.

Complete code:

from jupyter_dash import JupyterDash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State, ClientsideFunction
import dash_bootstrap_components as dbc
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
import plotly.graph_objs as go
import plotly.express as px

import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)

# jupyterdash setup
app = JupyterDash(external_stylesheets=[dbc.themes.SLATE])

# data and figure setup
df = px.data.stocks()
df = df.set_index('date')
fig1 = px.line(df, x = df.index, y = df.columns, template = 'plotly_dark')
fullnames = {'GOOG':'Google',
             'AAPL': 'Apple',
             'AMZN': 'Amazon',
             'FB':'Facebook',
             'NFLX':'Netflix',
             'MSFT':'Microsoft'}

# app layout
app.layout = dbc.Container([
    dbc.Row([
        # https://hackerthemes.com/bootstrap-cheatsheet/
        dbc.Col([html.H1("DropDown < CheckList", className = 'text-left text-success'), ], width = 8),

    ]),
    
    dbc.Row([dbc.Col([dcc.Checklist(id = 'Check1', 
                                           options=[{"label": fullnames[col], "value": col} for col in df.columns],
                                           value=df.columns),
                                        ], width = 2),
             
            dbc.Col([dcc.Graph(id='Graph1', figure = fig1)], width = 10),
             ],
    ),

])

# interactivity through callbacks
@app.callback(
    Output('Graph1', 'figure'),
    [Input('Check1', 'value')])
def subset_graph(value):
   
    dfs = df[value]
    fig2 = px.line(dfs, x = dfs.index, y = dfs.columns, template = 'plotly_dark')
    return fig2


app.run_server(mode='inline', port = 9099)

Suggestion 2


Following up on a comment from John Collins, I've wrapped the dcc.Checklist into a html.Div with the following setup to make the checklist scrollable when there are to many items to be displayed at the same time:

html.Div(dcc.Checklist(id = 'Check1', 
                       options=[{"label": col, "value": col} for col in df.columns],
                       value=df.columns,
                       labelStyle={'display': 'inline-block', 'width': '12em', 'line-height':'0.5em'}
                                            ),
        style = {"overflow-y":"scroll",
                   "overflow-x":'hidden',
                   "height": '480px'
                   }
        )

Plot 2:

enter image description here

Complete code for Plot 2:

from jupyter_dash import JupyterDash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State, ClientsideFunction
import dash_bootstrap_components as dbc
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
import plotly.graph_objs as go
import plotly.express as px
import numpy as np

import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)

# jupyterdash setup
app = JupyterDash(external_stylesheets=[dbc.themes.SLATE])

# data and figure setup
# data
start = 1980
ncols = 40
nrows = 365
cols = [str(i) for i in np.arange(start, start+ncols)]
df = pd.DataFrame(np.random.randint(-1,2, (nrows,ncols)), columns = cols).cumsum()
df.iloc[0] = 0

# figure
fig1 = px.line(df, x=df.index, y=cols,
#               width=820,
              height=480,
              template = 'plotly_dark'
             )

# app layout
app.layout = dbc.Container([
    dbc.Row([
        # https://hackerthemes.com/bootstrap-cheatsheet/
        dbc.Col([html.H1("DropDown < CheckList", className = 'text-left text-success', style = {'padding': 25})], width = 8),

    ]),
    
    dbc.Row([dbc.Col([html.Div(dcc.Checklist(id = 'Check1', 
                                           options=[{"label": col, "value": col} for col in df.columns],
                                           value=df.columns,
                                           labelStyle={'display': 'inline-block', 'width': '12em', 'line-height':'0.5em'}
                                            ),
                      style = {"overflow-y":"scroll",
                               "overflow-x":'hidden',
                               "height": '480px'
                              }
                              )], width = 3),
             
            dbc.Col([dcc.Graph(id='Graph1', figure = fig1)], width = 8),
             ],
           ),
])

# interactivity through callbacks
@app.callback(
    Output('Graph1', 'figure'),
    [Input('Check1', 'value')])
def subset_graph(value):
    dfs = df[value]
    fig2 = px.line(dfs, x = dfs.index, y = dfs.columns, template = 'plotly_dark')
    return fig2


app.run_server(mode='inline', port = 9099)

Upvotes: 2

Related Questions