šŸ“£ Introducing Dash `/pages` - A Dash 2.x Feature Preview

@snowde You should be able to open specific tabsā€¦ Have you tried it with the plug-in it and it didā€™t work? can you give an example of how you are doing this currently?

Hereā€™s a new feature request:

This plug-in doesnā€™t yet support pages in nested folders in pages/ . For example, if you have this structure in pages:

- pages
   |-- home.py
   -chapter1
     |-- page1.py
     |-- page2.py
   -chapter2
     |-- page1.py
     |-- page2.py

Iā€™m not sure about the best way to handle this, but it might be convenient if dash.page_registry would also have a folder variable that would be populated automatically. For the example above ā€œpages.chapter1.app1ā€ would also have folder=ā€œchapter1ā€.

(Note ā€“ in this potential solution, the dash.page_registry dict is flat, with a key for each app in pages/)

Then if you were populating submenus for the app, you could create them like this:

html.Div(
    [
        html.Div(dcc.Link(page["name"], href=page["path"]))
        for page in dash.page_registry
        if page["folder"] == "chapter1"
    ]
)

Does anyone have any thoughts or see a better way to do this?

1 Like

Looks great! It looks similar to this project which creates a template of a multipage dash app https://github.com/ned2/slapdash

That project hasnā€™t been updated for around 2 years, so doesnā€™t contain the new features of dash 2. Would be really cool to have this new multipage code in a batteries included template. Is there something like this for dash 2 yet?

3 Likes

This is really exciting and definitely reduces the amount of steps needed to build a multi-page app. I took @Emil 's cool Burger component and built a very simple example app.

One area I wonder whether we can enhance is sharing data between pages. Now Iā€™m using dcc.Store and callbacks, but perhaps there are quicker/simpler ways. @AnnMarieW and I were talking and she was thinking maybe we could do it through the dash.register.pages ā€“ make it so a dcc.Store is generated automatically through variable name or id.

This app has two pages and the main app.py. See structure below:

home.py (Meat)

import dash
from dash import html, dcc, callback, Output, Input
import pandas as pd
import plotly.express as px

df = pd.read_csv(
    "https://gist.githubusercontent.com/chriddyp/c78bf172206ce24f77d6363a2d754b59/raw/c353e8ef842413cae56ae3920b8fd78468aa4cb2/usa-agricultural-exports-2011.csv"
)
print(df.head())

dash.register_page(
    __name__,
    path="/",
    name="Meat",
    description="Welcome to my app",
    order=0,
    icon="fa fa-ambulance",
)

layout = html.Main(
    children=[
        html.H1("US Meat Exports (2011)"),
        html.Div(
            dcc.Dropdown(
                id="state-slctd",
                multi=False,
                value=df.state[4],
                options=[{"label": x, "value": x} for x in df.state],
                style={"width": "60%", "color": "#000000"},
                persistence_type="session",
                persistence=True,
            ),
            style={"display": "flex", "justifyContent": "center"},
        ),
        html.Div(
            id="graph-placeholder",
            style={"display": "flex", "justifyContent": "center"},
        ),
    ],
    style={"width": "100%", "height": "100vh"},
)


@callback(
    Output("graph-placeholder", "children"),
    Output("stored-state", "data"),
    Input("state-slctd", "value"),
)
def update_graph(selected):
    dff = df[df.state == selected]
    fig = px.bar(dff, x=["beef", "pork", "poultry"])
    return dcc.Graph(figure=fig, style={"width": "60%"}), selected

agri.py (Agriculture)

import dash
from dash import html, dcc, callback, Input, Output
import pandas as pd
import plotly.express as px

df = pd.read_csv(
    "https://gist.githubusercontent.com/chriddyp/c78bf172206ce24f77d6363a2d754b59/raw/c353e8ef842413cae56ae3920b8fd78468aa4cb2/usa-agricultural-exports-2011.csv"
)

dash.register_page(
    __name__, name="Agriculture", path="/agriculture", icon="fa fa-fw fa-heart-o"
)


def layout():
    return html.Main(
        [
            html.H1("US Agricultural Exports (2011)"),
            html.Div(
                id="graph-placeholder2",
                style={"display": "flex", "justifyContent": "center"},
            ),
        ],
        style={"width": "100%", "height": "100vh"},
    )


@callback(
    Output("graph-placeholder2", "children"),
    Input("stored-state", "data"),
)
def update_graph2(selected):
    print(selected)
    dff = df[df.state == selected]
    fig = px.bar(dff, x=["corn", "wheat", "cotton"], title=f"{selected}")
    return dcc.Graph(figure=fig, style={"width": "60%"})

And the main app.py

from dash import Dash, html, dcc
import dash
import pages_plugin
from dash_extensions import Burger

# Example CSS from the original demo.
external_css = [
    "https://negomi.github.io/react-burger-menu/example.css",
    "https://negomi.github.io/react-burger-menu/normalize.css",
    "https://negomi.github.io/react-burger-menu/fonts/font-awesome-4.2.0/css/font-awesome.min.css",
]

app = Dash(__name__, plugins=[pages_plugin], external_stylesheets=external_css)
# print(list(dash.page_registry.values())[1]['icon'])


app.layout = html.Div(
    [
        Burger(
            children=[
                html.Nav(
                    children=[
                        dcc.Link(
                            [html.I(className=page["icon"]), html.Span(page["name"])],
                            href=page["path"],
                            className="bm-item",
                            style={"display": "block"},
                        )
                        for page in dash.page_registry.values()
                    ],
                    className="bm-item-list",
                    style={"height": "100%"},
                )
            ]
        ),
        pages_plugin.page_container,
        dcc.Store(id="stored-state"),
    ],
    style={"height": "100%"},
)


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

2 Likes

Hey @adamschroeder

Thanks for posting another nice example. A few cool things here:

  • This shows how to have names for the pages that are different than your file names, and how to include other page specific data such as icons:

For example agri.py sets the name ā€œAgricultureā€ and adds the icon here:

dash.register_page(
    __name__, name="Agriculture", path="/agriculture", icon="fa fa-fw fa-heart-o"
)

and in app.py itā€™s used to set the label on the link:

dcc.Link( [html.I(className=page["icon"]), html.Span(page["name"])], ....
  • Nice use of @Emil 's burger menu!

  • For dash-bootstrap-component users, you can also use the new dbc.Offcanvas component ā€“ new in V1.0. See more info here and itā€™s now easier to add icons

2 Likes

2 posts were split to a new topic: Attempting to connect a callback Input item but no component with that ID exists

Great new feature! Am working with this, but am coming across a weird error. Iā€™m trying to build a landing page in a seperate file, but its not picking up the full list of pages that are registered.

I assume this is something to do with the order in which register_page is called - so when layout() is run in my file the page_registry doesnā€™t have all of the pages registered yet?

Do you have to have the landing page defined in app.py? When its in app.py it seems to work perfectlyā€¦

Or is it something to do with the order=0 parameter in register_page?

Regards,
Amien

Hi @pandamodium

Itā€™s possible to build the landing page in a separate file. Can you say more about how you have things set up?

You can also find some examples here

Hey Ann!

Thanks for replying! Its a basic app with a few pages in a ā€œpagesā€ folder.

I put the below code in pages/home.py - but the links are only for the first few pages. It looks like it runs in alphabetical order, but only up to the page in question.

app.layout = html.Div(
    [
        html.H1("App Frame"),
        html.Div(
            dcc.Link("Go back home", href=dash.page_registry["pages.home"]["path"])
        ),
        html.Div(
            [
                html.Div(
                    dcc.Link(f"{page['name']} - {page['path']}", href=page["path"])
                )
                for page in dash.page_registry.values()
                if page["module"] != "pages.not_found_404"
            ]
        ),
        dl.plugins.page_container,
    ]
)

Hi @pandamodium

There is still not enough info in your post for me to help. The pages are all registered when the app starts, so timing shouldnā€™t be an issue.

Here are a couple debugging tips:

You can use your IDEā€™s debugger tool to inspect dash.page_registry or you can do it the ā€œhard wayā€ and print it.

if you simply use: print(dash.page_registry), you may get something thatā€™s pretty hard to read. To print it in a nicer format, , I like to remove the layout key in the nested dict and format it with json.dumps Hereā€™s an example:

Note - Dash pages is now available in dash-labs v1.0.0 as a plug-in. (pip install dash-labs)

import dash_labs as dl

app = Dash(__name__, plugins=[dl.plugins.pages])

# ---  debugging - print `dash.page_registry`
import json
registry = {a:{c:d for c, d in b.items() if c != 'layout'} for a, b in dash.page_registry.items()}
print(json.dumps(registry, indent=4))
#  ------------------------------------

...

Is the page you are looking for not in dash.page_registry? How did you define the dash.register_page in home.py? Adding path="/" will make it the landing page:

dash.register_page(__name__, path="/")
1 Like

Thanks AnnMarieW!

I put that code in - and was still getting the same effect, but I think it worked it out!

The solution was to make sure the layout definition was a function. so my home.py becomes:

def layout():
	return  dbc.Container([
	html.Div(
		dcc.Link('Go back home', href=dash.page_registry['pages.home']['path'])
	),

	html.Div([
		html.Div(dcc.Link(
			f"{page['name']} - {page['path']}",
			href=page['path']
		))
		for page in dash.page_registry.values()
		if page['module'] != 'pages.not_found_404'
	]),
])

It seems that if you have the home page in a separate page - it will generate the layout objects in the python files in the ā€œpagesā€ folder when you start (in alphabetical order), and so if your file is called home.py, nothing after H will be in the page_registry at the time that code is run.

Anyway if anyone else has this issue - just remember to use a function for the layout object.

Regards,
Amien

@pandamodium

Itā€™s not supposed to work that way, so this might be a bug. You should be able to define any of the pages to be the home page without having to make the layout a function.

The code I provided was to help with debugging and not to fix it. It would be very helpful if you shared what was printed and how you had dash.register_page(...) defined in each of the apps in the pages/

Hey Ann!

Sure thing - see below for a stripped down version showing the behaviour in practice:

image

pages/home.py:

import dash
from dash import Dash, html, dcc,callback, Input, Output, State	
import dash_bootstrap_components as dbc

# ---  debugging - print `dash.page_registry`
import json
registry = {a:{c:d for c, d in b.items() if c != 'layout'} for a, b in dash.page_registry.items()}
print(json.dumps(registry, indent=4))
#  ------------------------------------

dash.register_page(
	__name__,
	path='/',
	name='Old Landing',
	description='Welcome to Athena',
	order=0,
	redirect_from=['/old-home-page', '/v2'],
	extra_template_stuff='yup'
)

layout = dbc.Container([
	html.Div(
		dcc.Link('Go back home', href=dash.page_registry['pages.home']['path'])
	),

	html.Div([
		html.Div(dcc.Link(
			f"{page['name']} - {page['path']}",
			href=page['path']
		))
		for page in dash.page_registry.values()
		if page['module'] != 'pages.not_found_404'
	]),
])

app.py:

from dash import Dash, html, dcc
import dash
import pages_plugin

app = Dash(__name__, plugins=[pages_plugin])


dash.register_page('another_page', layout='Another page', path='/another-page')
dash.register_page('and_again', layout='And again!', path='/and-again')


app.layout = html.Div([
	html.H1('App Frame'),

	pages_plugin.page_container

])



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

The result from running python app.py:
image

Apologies and the logging output was:

Dash is running on http://127.0.0.1:8111/

 * Serving Flask app 'app' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
{
    "pages.analyticapps": {
        "module": "pages.analyticapps",
        "supplied_path": "/oldhome",
        "path": "/oldhome",
        "supplied_name": "Analytic Apps",
        "name": "Analytic Apps",
        "supplied_title": null,
        "title": "Analytic Apps",
        "supplied_description": "Welcome to my app",
        "description": "Welcome to my app",
        "supplied_order": 0,
        "supplied_layout": null,
        "extra_template_stuff": "yup",
        "image": "app.jpeg",
        "supplied_image": null,
        "redirect_from": null
    },
    "pages.historical_archive": {
        "module": "pages.historical_archive",
        "supplied_path": null,
        "path": "/historical-archive",
        "supplied_name": null,
        "name": "Historical archive",
        "supplied_title": null,
        "title": "Historical archive",
        "supplied_description": null,
        "description": "Historical archive",
        "supplied_order": null,
        "supplied_layout": null,
        "image": "app.jpeg",
        "supplied_image": null,
        "redirect_from": null
    }
}
{
    "pages.analyticapps": {
        "module": "pages.analyticapps",
        "supplied_path": "/oldhome",
        "path": "/oldhome",
        "supplied_name": "Analytic Apps",
        "name": "Analytic Apps",
        "supplied_title": null,
        "title": "Analytic Apps",
        "supplied_description": "Welcome to my app",
        "description": "Welcome to my app",
        "supplied_order": 0,
        "supplied_layout": null,
        "extra_template_stuff": "yup",
        "image": "app.jpeg",
        "supplied_image": null,
        "redirect_from": null
    },
    "pages.historical_archive": {
        "module": "pages.historical_archive",
        "supplied_path": null,
        "path": "/historical-archive",
        "supplied_name": null,
        "name": "Historical archive",
        "supplied_title": null,
        "title": "Historical archive",
        "supplied_description": null,
        "description": "Historical archive",
        "supplied_order": null,
        "supplied_layout": null,
        "image": "app.jpeg",
        "supplied_image": null,
        "redirect_from": null
    }
}

@pandamodium
Thanks so much for sharing the output, I think see whatā€™s going on now.

The links should be populated in app.py:

html.Div([
		html.Div(dcc.Link(
			f"{page['name']} - {page['path']}",
			href=page['path']
		))
		for page in dash.page_registry.values()
		if page['module'] != 'pages.not_found_404'
	]),

And this bit of debugging code below should go right before the layout in app.py. If you put it in one of the apps in pages/, it will only show whatā€™s been added up to that point.

To confirm my theory, could you give it a try? Itā€™s also odd that "pages.analyticapps" shows up twice in the dict output you provided.

# ---  debugging - print `dash.page_registry`
import json
registry = {a:{c:d for c, d in b.items() if c != 'layout'} for a, b in dash.page_registry.items()}
print(json.dumps(registry, indent=4))
#  ------------------------------------

Hey Ann!

Yes thatā€™s exactly what happens!

Unless you make the Layout in home.py a function, in which case it updates the layout each time it is called and populates the full list.

And I think analytics app is being repeated because I have debug=True, Iā€™ve noticed it duplicates all my logging stuff if I have that on.

Will paste the logs later this morning for you to review.

Thanks again for all your help!

Regards
Amien

2 Likes

Hi all,
This is so timely! Iā€™m working on a multi-page app that takes into consideration our physical and labor capacity and need to show different views. I was struggling to try to figure out a way to use the tabs and make it work. Iā€™ve gotten a few pages stitched together and noticed one thing.

I have persistence turned on for many of my inputs but I noticed that whenever I return back to that page the data is not storing. I have persistence turned on and it worked previously, but now that Iā€™m using a multi page app the data is not persisting when I switch back to the link. Is there a way to resolve this issue or is it a bug?

dcc.Dropdown(
        id='auction_dropdown',
        options=[{'label': auction, 'value': auction}
                 for auction in ['AAAW', 'FAAO', 'SVAA', 'MAA', 'BWAE', 'GCAA', 'PXAA', 'DFWA', 'BCAA', 'GOAA']],
        value=['AAAW', 'FAAO', 'SVAA', 'MAA', 'BWAE', 'GCAA', 'PXAA', 'DFWA', 'BCAA', 'GOAA'],  # Default value to show
        persistence=True,
        multi=True,
        searchable=False
    ),

Hi @johnkangw

Welcome to the Dash community and thanks for trying the new pages plug-in :slight_smile:

I just tried this and it worked fine for me ā€“ selections persisted when changing pages. I just copied your dropdown into one of the apps in the pages folder in this dash-labs example.

Can you provide a minimal example that replicates the issue?

Ann,
Sure thing!

I have a separate page in (/pages) with a DataTable that takes in a dataframe (ws_retail_forecast) with persistence turned to True.

    dash_table.DataTable(
        id='table_retail',
        columns=[{'id': i, 'name': i, "format": {
            "specifier": ".1f"}} for i in (ws_retail_forecast.reset_index()).columns],
        data=(ws_retail_forecast.reset_index()).to_dict('records'),
        editable=True,
        persistence=True
    ),

The issue is that whenever I navigate away from the page and go back the data does not persist.

Second question:
Iā€™m using the manual ā€˜pages_plugin.pyā€™ in when I startup the app

app = Dash(__name__, plugins=[pages_plugin], external_stylesheets=[dbc.themes.BOOTSTRAP, dbc_css])

The issue is that when I try to use the code from the dash_labs

app = Dash(__name__, plugins=[dl.plugins.pages], external_stylesheets=[dbc.themes.BOOTSTRAP, dbc_css])```

I get an error for this line of the app.,py file:
html.Div(
    dcc.Link('Go back home', href=dash.page_registry['pages.home']['path'])
),
The error is:

File ā€œC:/Users/Jkang1/Cox Automotive/Recon Industrial Engineering Team - General/Capacity Model/main_python_files/app.pyā€, line 29, in
dcc.Link(ā€˜Go back homeā€™, href=dash.page_registry[ā€˜pages.homeā€™][ā€˜pathā€™])
KeyError: ā€˜pages.homeā€™

Maybe dash_labs can't find pages.home? I have the home.py file in the pages folder.

Hi @johnkangw

hmmā€¦ looks right to me. In your home.py file do you have:

dash.register_page( __name__, path="/")

One way to help with debugging is to include the code below in app.py. Place it right before the layout. (it will show whatā€™s included in dash.page_registry:


import json
registry = {a:{c:d for c, d in b.items() if c != 'layout'} for a, b in dash.page_registry.items()}
print(json.dumps(registry, indent=4))

I havenā€™t used persistence with the datatable. Does the dropdown work for you?