Show and Tell - Dash Bootstrap Components


#1

Hi everyone,

We’d like to present our library of Bootstrap themed components for Dash.

We have documentation available here, which we’re still adding to and improving, but there should already be enough there to give you a flavour of what the library can do and to get you started. We’re also working on some proper example apps in addition to the documentation.

The source code is all available on Github. We welcome bug reports and pull requests from anyone who would like to contribute!

@chriddyp if you could add us to the meta thread that would be great!


Dash Bootstrap and DataGrid components/contrib?
#2

WOW, this is very useful!

As a developer who only knows the basics of front end development, something like this allows me to do a lot more with dash!

Thanks for contributing!


#3

Agreed, this is pretty amazing. I totally see integrating this in to a few of our apps, thanks!


#4

@tcbegley Just started with Dash and happened to come across this.

I would like to begin my testing of dash while using your bootstrap components. I saw the documentation and was curious if you have any built-out examples that go a little deeper to use as a reference when learning/testing.

Many thanks for building this!


#5

Hey @ndbcorp,

We don’t really have many built-out examples right now unfortunately, but it’s very much on our to-do list and hopefully we’ll get a few out there in the next few weeks.

In the meantime if you have specific questions about dash-bootstrap-components I’m happy to answer them here.


#6

First question i have is about the navbar and how it can be used to navigate a multi page app. I will refer to the demo layout you have in the documentation.

Lets say in the menu drop down, clicking Entry 1 would display what your current demo layout shows (Heading + Graph). Now clicking Entry 2 would show lets says the dcc upload component and a corresponding dash datatable. Can you show how to code this? How about having tabs within entry 2…is it possible?

The dash Tabs + Subtabs doesnt really work well for what i am looking for. See thread on this here: Dash Tabs and SubTabs


#7

The NavLink and DropdownItem components can work just like a dash-core-components style Link, which you can use in conjunction with dcc.Location to re-render the layout based on the current url. Checkout the Dash docs for more detail on that.

Here’s a version of the app you described. It’s definitely possible to have multiple pages and then have a set of tabs on one of the pages. I used some simple cards for the different layouts so as to not complicate things, but you can replace them with whatever you like, including the upload component, or graphs, or whatever else you like.

import dash
import dash_bootstrap_components as dbc
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output

app = dash.Dash(external_stylesheets=[dbc.themes.BOOTSTRAP])

navbar_children = [
    dbc.NavItem(dbc.NavLink("Page 1", href="/page-1")),
    dbc.NavItem(dbc.NavLink("Page 2", href="/page-2")),
]

navbar = dbc.Navbar(navbar_children, brand="Example", sticky="top")

# define content for page 1
page1 = dbc.Card(
    [
        dbc.CardTitle("Page 1 contents"),
        dbc.CardText("You can replace this with whatever you like"),
    ],
    body=True,
)

# define content for page 2

tab1 = dbc.Card(
    [
        dbc.CardTitle("Page 2, tab 1 contents"),
        dbc.CardText("You can replace this with whatever you like"),
    ],
    body=True,
)

tab2 = dbc.Card(
    [
        dbc.CardTitle("Page 2, tab 1 contents"),
        dbc.CardText("Let's write something different here for fun"),
    ],
    body=True,
)

page2 = dbc.Tabs(
    [
        dbc.Tab(tab1, label="Tab 1", className="mt-3"),
        dbc.Tab(tab2, label="Tab 2", className="mt-3"),
    ]
)

# define page layout
app.layout = html.Div(
    [
        dcc.Location(id="url", pathname="/page-1"),
        navbar,
        dbc.Container(id="content", style={"padding": "20px"}),
    ]
)


# create callback for modifying page layout
@app.callback(Output("content", "children"), [Input("url", "pathname")])
def display_page(pathname):
    if pathname == "/page-1":
        return page1
    if pathname == "/page-2":
        return page2
    # if not recognised, return 404 message
    return html.P("404 - page not found")


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

You can link between pages from a DropdownMenu embedded in the NavBar too, just set the href property of each DropdownMenuItem component and use it in the same way in the callback. Hope that makes sense, let me know if you’re having problems.

As your app grows, you should probably also split the above across multiple files. See the “Structuring a Multi-Page App” section of the Dash docs.


#8

Thanks for the quick reply. I tried modifying the code you provided, adding the dcc upload + datatable from
the first example at https://dash.plot.ly/dash-core-components/upload.

I added/updated the following packages:

import dash_table
from dash.dependencies import Input, Output, State

In your code, i replaced the page1 variable with

page1 = html.Div([
    dcc.Upload(
        id='upload-data',
        children=html.Div([
            'Drag and Drop or ',
            html.A('Select Files')
        ]),
        style={
            'width': '100%',
            'height': '60px',
            'lineHeight': '60px',
            'borderWidth': '1px',
            'borderStyle': 'dashed',
            'borderRadius': '5px',
            'textAlign': 'center',
            'margin': '10px'
        },
        # Allow multiple files to be uploaded
        multiple=True
    ),
    html.Div(id='output-data-upload'),
])

and i added the following at the bottom for functionality:

def parse_contents(contents, filename, date):
    content_type, content_string = contents.split(',')

    decoded = base64.b64decode(content_string)
    try:
        if 'csv' in filename:
            # Assume that the user uploaded a CSV file
            df = pd.read_csv(
                io.StringIO(decoded.decode('utf-8')))
        elif 'xls' in filename:
            # Assume that the user uploaded an excel file
            df = pd.read_excel(io.BytesIO(decoded))
    except Exception as e:
        print(e)
        return html.Div([
            'There was an error processing this file.'
        ])

    return html.Div([
        html.H5(filename),
        html.H6(datetime.datetime.fromtimestamp(date)),

        dash_table.DataTable(
            data=df.to_dict('rows'),
            columns=[{'name': i, 'id': i} for i in df.columns]
        ),

        html.Hr(),  # horizontal line

        # For debugging, display the raw contents provided by the web browser
        html.Div('Raw Content'),
        html.Pre(contents[0:200] + '...', style={
            'whiteSpace': 'pre-wrap',
            'wordBreak': 'break-all'
        })
    ])


@app.callback(Output('output-data-upload', 'children'),
              [Input('upload-data', 'contents')],
              [State('upload-data', 'filename'),
               State('upload-data', 'last_modified')])
def update_output(list_of_contents, list_of_names, list_of_dates):
    if list_of_contents is not None:
        children = [
            parse_contents(c, n, d) for c, n, d in
            zip(list_of_contents, list_of_names, list_of_dates)]
        return children

It didn’t work. Am i doing something wrong here? I am getting the following error:

dash.exceptions.NonExistantIdException:
Attempting to assign a callback to the
component with the id "output-data-upload" but no
components with id "output-data-upload" exist in the
app's layout.


Here is a list of IDs in layout:
['url', 'content']


If you are assigning callbacks to components
that are generated by other callbacks

(and therefore not in the initial layout), then
you can suppress this exception by setting
`app.config['suppress_callback_exceptions']=True`.

#9

You’re creating a callback that references a component that is not present in the layout when the app loads (specifically ‘output-data-upload’ is on the second page, so Dash doesn’t know about it). It throws the error to warn you in case you might be doing something wrong.

You can configure Dash to ignore when this happens, and not complain when you reference elements in callbacks that aren’t present in the layout. This is essential if you are assigning callbacks to components that are added to the layout at runtime, such as in a multi-page app.

TL;DR - Just add app.config.suppress_callback_exceptions = True after creating the app object and it should work (or at least throw a different error).


#10

Got it, thanks. I was able to get it to work…however the uploaded table disappears if i navigate to another page and then come back. Is this a due to the dbc library or dash?


#11

That’s to do with how Dash works rather than specific to dash-bootstrap-components. If you want the table to persist after being uploaded, you’ll have to cache the data somehow, then get Dash to retrieve it every time you visit Page 1 again.

Here’s an example using dcc.Store. I did this pretty quickly, so there might be a better way to set it up, but it works:

Modify app.layout to

app.layout = html.Div(
    [
        dcc.Location(id="url", pathname="/page-1"),
        dcc.Store(id="upload-storage"),
        navbar,
        dbc.Container(id="content", style={"padding": "20px"}),
    ]
)

When a file is uploaded, we’ll serialise it and save it to upload-storage. Modify parse_contents to the following:

def parse_contents(contents, filename, date):
    content_type, content_string = contents.split(",")
    decoded = base64.b64decode(content_string)
    try:
        if "csv" in filename:
            # Assume that the user uploaded a CSV file
            df = pd.read_csv(io.StringIO(decoded.decode("utf-8")))
        elif "xls" in filename:
            # Assume that the user uploaded an excel file
            df = pd.read_excel(io.BytesIO(decoded))
    except Exception as e:
        print(e)
        return html.Div(["There was an error processing this file."])

    return {
        "filename": filename,
        "df": df.to_dict(orient="rows"),
        "date": date,
    }

Then create a callback to populate upload-storage when a file or files are uploaded:

@app.callback(
    Output("upload-storage", "data"),
    [Input("upload-data", "contents")],
    [State("upload-data", "filename"), State("upload-data", "last_modified")],
)
def store_upload(list_of_contents, list_of_names, list_of_dates):
    if list_of_contents is not None:
        children = [
            parse_contents(c, n, d)
            for c, n, d in zip(list_of_contents, list_of_names, list_of_dates)
        ]
        return children

Finally we want to use data from upload-storage when the page is loaded. Let’s first make a function that generates page 1 from the data instead of having it defined as a global variable, so replace page1 = ... with

def make_page1(data):
    if data is not None:
        uploaded_data = []
        for d in data:
            filename = d["filename"]
            df = pd.DataFrame(d["df"])
            date = d["date"]
            uploaded_data.append(
                html.Div(
                    [
                        html.H5(filename),
                        html.H6(datetime.datetime.fromtimestamp(date)),
                        dash_table.DataTable(
                            data=df.to_dict("rows"),
                            columns=[{"name": i, "id": i} for i in df.columns],
                        ),
                    ]
                )
            )
    else:
        uploaded_data = None
    return html.Div(
        [
            dcc.Upload(
                id="upload-data",
                children=html.Div(
                    ["Drag and Drop or ", html.A("Select Files")]
                ),
                style={
                    "width": "100%",
                    "height": "60px",
                    "lineHeight": "60px",
                    "borderWidth": "1px",
                    "borderStyle": "dashed",
                    "borderRadius": "5px",
                    "textAlign": "center",
                    "margin": "10px",
                },
                # Allow multiple files to be uploaded
                multiple=True,
            ),
            html.Div(uploaded_data, id="output-data-upload"),
        ]
    )

Finally we want this to get called when the pathname changes, so change the url callback to

@app.callback(
    Output("content", "children"),
    [Input("url", "pathname"), Input("upload-storage", "data")],
)
def display_page(pathname, data):
    if pathname == "/page-1":
        return make_page1(data)
    if pathname == "/page-2":
        return page2
    # if not recognised, return 404 message
    return html.P("404 - page not found")

Hope that all makes sense. Let me know if you can’t get it to work.


#12

At first when uploading a file on page 1, no table shows. The only way to get the table to display is to navigate to page 2 and then return to page 1.

import dash
import dash_bootstrap_components as dbc
import dash_core_components as dcc
import dash_html_components as html
import dash_table
from dash.dependencies import Input, Output, State

import base64
import datetime
import io
import pandas as pd

app = dash.Dash(external_stylesheets=[dbc.themes.BOOTSTRAP])
app.config.suppress_callback_exceptions = True

navbar_children = [
    dbc.NavItem(dbc.NavLink("Page 1", href="/page-1")),
    dbc.NavItem(dbc.NavLink("Page 2", href="/page-2")),
    html.Div(html.Img(src='DeepDive_Logo_V1_Horizontal.png',height="100%"))
]

navbar = dbc.Navbar(navbar_children, brand="Example", sticky="top")

# define content for page 1
def make_page1(data):
    if data is not None:
        uploaded_data = []
        for d in data:
            filename = d["filename"]
            df = pd.DataFrame(d["df"])
            date = d["date"]
            uploaded_data.append(
                html.Div(
                    [
                        html.H5(filename),
                        html.H6(datetime.datetime.fromtimestamp(date)),
                        dash_table.DataTable(
                            data=df.to_dict("rows"),
                            columns=[{"name": i, "id": i} for i in df.columns],
                        ),
                    ]
                )
            )
    else:
        uploaded_data = None
    return html.Div(
        [
            dcc.Upload(
                id="upload-data",
                children=html.Div(
                    ["Drag and Drop or ", html.A("Select Files")]
                ),
                style={
                    "width": "100%",
                    "height": "60px",
                    "lineHeight": "60px",
                    "borderWidth": "1px",
                    "borderStyle": "dashed",
                    "borderRadius": "5px",
                    "textAlign": "center",
                    "margin": "10px",
                },
                # Allow multiple files to be uploaded
                multiple=True,
            ),
            html.Div(uploaded_data, id="output-data-upload"),
        ]
    )


# define content for page 2

tab1 = dbc.Card(
    [
        dbc.CardTitle("Page 2, tab 1 contents"),
        dbc.CardText("You can replace this with whatever you like"),
    ],
    body=True,
)

tab2 = dbc.Card(
    [
        dbc.CardTitle("Page 2, tab 1 contents"),
        dbc.CardText("Let's write something different here for fun"),
    ],
    body=True,
)

page2 = dbc.Tabs(
    [
        dbc.Tab(tab1, label="Tab 1", className="mt-3"),
        dbc.Tab(tab2, label="Tab 2", className="mt-3"),
    ]
)

# define page layout
app.layout = html.Div(
    [
        dcc.Location(id="url", pathname="/page-1"),
        dcc.Store(id="upload-storage"),
        navbar,
        dbc.Container(id="content", style={"padding": "20px"}),
    ]
)


# create callback for modifying page layout
@app.callback(
    Output("content", "children"),
    [Input("url", "pathname"), Input("upload-storage", "data")],
)
def display_page(pathname, data):
    if pathname == "/page-1":
        return make_page1(data)
    if pathname == "/page-2":
        return page2
    # if not recognised, return 404 message
    return html.P("404 - page not found")
 # upload to datatable
def parse_contents(contents, filename, date):
    content_type, content_string = contents.split(",")
    decoded = base64.b64decode(content_string)
    try:
        if "csv" in filename:
            # Assume that the user uploaded a CSV file
            df = pd.read_csv(io.StringIO(decoded.decode("utf-8")))
        elif "xls" in filename:
            # Assume that the user uploaded an excel file
            df = pd.read_excel(io.BytesIO(decoded))
    except Exception as e:
        print(e)
        return html.Div(["There was an error processing this file."])

    return {
        "filename": filename,
        "df": df.to_dict(orient="rows"),
        "date": date,
    }
@app.callback(
    Output("upload-storage", "data"),
    [Input("upload-data", "contents")],
    [State("upload-data", "filename"), State("upload-data", "last_modified")],
)
def store_upload(list_of_contents, list_of_names, list_of_dates):
    if list_of_contents is not None:
        children = [
            parse_contents(c, n, d)
            for c, n, d in zip(list_of_contents, list_of_names, list_of_dates)
        ]
        return children

@app.callback(Output('output-data-upload', 'children'),
              [Input('upload-data', 'contents')],
              [State('upload-data', 'filename'),
               State('upload-data', 'last_modified')])
def update_output(list_of_contents, list_of_names, list_of_dates):
    if list_of_contents is not None:
        children = [
            parse_contents(c, n, d) for c, n, d in
            zip(list_of_contents, list_of_names, list_of_dates)]
        return children

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

#13

Delete this function and it should work

@app.callback(Output('output-data-upload', 'children'),
              [Input('upload-data', 'contents')],
              [State('upload-data', 'filename'),
               State('upload-data', 'last_modified')])
def update_output(list_of_contents, list_of_names, list_of_dates):
    if list_of_contents is not None:
        children = [
            parse_contents(c, n, d) for c, n, d in
            zip(list_of_contents, list_of_names, list_of_dates)]
        return children

It seems to be because both update_output and make_page1 are trying to modify output-data-upload.


#14

@tcbegley This is pretty cool. I will continue to test this over the next couple weeks.

Question about customizing the navbar. In the example in your documentation, i tried adding a logo to the top left of the navbar (a .png file located in assests folder) & move the name of the app to the center of the navbar. Cant seem to get this to work properly. Could you show how to do this?


#15

Hey @sriotti

Unfortunately the navbar is not super customisable. We had to make some trade-offs between customisability and simplicity in the interface.

You could manually construct what you’re describing though using html components and applying the relevant Bootstrap classes. For example, I downloaded this logo and saved it to assets/logo.png then built the following app:

import dash
import dash_bootstrap_components as dbc
import dash_core_components as dcc
import dash_html_components as html

app = dash.Dash(external_stylesheets=[dbc.themes.BOOTSTRAP])

dropdown = dbc.DropdownMenu(
    nav=True,
    in_navbar=True,
    label="Menu",
    children=[
        dbc.DropdownMenuItem("Entry 1"),
        dbc.DropdownMenuItem("Entry 2"),
        dbc.DropdownMenuItem(divider=True),
        dbc.DropdownMenuItem("Entry 3"),
    ],
)

logo = html.Img(src="/assets/logo.png", height="50px")

title = dcc.Link("Title", href="/", className="navbar-brand")

nav_items = html.Ul(
    [dbc.NavItem(dbc.NavLink("Page 1", href="/page-1")), dropdown],
    className="navbar-nav",
)

app.layout = html.Nav(
    dbc.Container(
        dbc.Row(
            [
                dbc.Col(logo, width="auto"),
                dbc.Col(title, width="auto"),
                dbc.Col(nav_items, width="auto"),
            ],
            justify="between",
            align="center",
            style={"width": "100%"},
        )
    ),
    className="navbar navbar-light navbar-expand-md bg-light sticky-top",
)

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

which gives me this


#16

Thanks for sharing. How about a sidebar like the one requested in the following post in Dash Tabs and SubTabs ? The linked post mentions the Shiny Server sidebar, which currently can’t be replicated using dash tabs + sub tabs in a visually appealing way.


#17

I’ve managed to do something like that in the past, but it’s a little too involved to reply with here, I’ll add one to the examples when we sort them out soon.


#18

Cool, I look forward to seeing it and the rest of your examples. Thanks again for working on this.


#19

Thanks, that ended up working. So this approach would be necessary for all pages if i would like to keep the previous state while navigating?

I have a question about the customizing style sheet section of documentation… I am trying to follow the hypothetical scenario you proposed of using the Cerulean theme. I added a boostrap.css file to my /assets folder that includes the Cerulean code from bootswatch. How do i select the different options within the theme? Do i have to add a className to my components in app.layout specifying? For instance, how do i get the navy blue navbar from the Cerulean theme as opposed the silver navbar?


#20

@ndbcorp yes, afaik when you modify the props of components in the layout it’s getting modified in the browser, I don’t believe that the modifications are being sent back to the Python object that created them on the server. Therefore when you remove these components from the layout, the modified props get lost. If you want them to persist you should cache them as above.

The navy navbar is Cerulean’s “dark” navbar. In dash-bootstrap-components you can do

dbc.Navbar(..., color="dark", dark=True)

The syntax looks a little redundant in this case, the dark=True tells dash-bootstrap-components that we’ve chosen a dark color (namely “dark” in this case!) and will choose a light text color to maintain a good contrast.

Doing that I see something like this:

Note: in case it wasn’t clear in the docs, the Cerulean bootstrap.css replaces the regular bootstrap.css. Reading the docs back I thought there could be potential for confusion there, but perhaps it’s fine.