Quick summary
Summarize this blog with AI
Dash connects Python functions to browser controls and Plotly charts. This tutorial builds one complete sales dashboard with a region selector and an empty state. The four rows are fictional; no external dataset, database, or API key is required.
Create an environment and install the tested versions
On macOS or Linux, run:
python3 -m venv .venv
.venv/bin/python -m pip install "dash==4.4.1" "plotly==7.1.0"
In Windows PowerShell, use:
py -m venv .venv
.venv\Scripts\python.exe -m pip install "dash==4.4.1" "plotly==7.1.0"
The example was tested with Python 3.12.14, Dash 4.4.1, and Plotly 7.1.0. If you use a different version, repeat the callback and browser checks rather than assuming matching behavior. Use the pip guide to select the correct environment interpreter.
Define the question and expected totals
The chart answers: “How much revenue did each product generate in the selected regions?” Each fixture row is a region/product total in whole US dollars. East contributes 120 + 80 = 200; West contributes 150 + 50 = 200. With both selected, Books totals 270, Games 130, and the overall revenue 400. Selecting no regions should not silently show all data.
Save this complete application as app.py
from dash import Dash, Input, Output, dcc, html
import plotly.graph_objects as go
SALES = [
{"region": "East", "product": "Books", "revenue": 120},
{"region": "East", "product": "Games", "revenue": 80},
{"region": "West", "product": "Books", "revenue": 150},
{"region": "West", "product": "Games", "revenue": 50},
]
def sales_figure(regions):
selected = set(regions or []) & {"East", "West"}
rows = [row for row in SALES if row["region"] in selected]
figure = go.Figure()
if not rows:
figure.add_annotation(
text="No regions selected", showarrow=False,
x=0.5, y=0.5, xref="paper", yref="paper",
)
figure.update_layout(title="Sales by product")
return figure, "No regions selected."
totals = {}
for row in rows:
totals[row["product"]] = totals.get(row["product"], 0) + row["revenue"]
products = sorted(totals)
figure.add_bar(x=products, y=[totals[p] for p in products])
figure.update_layout(
title="Sales by product", yaxis_title="Revenue (USD)",
margin=dict(l=45, r=15, t=55, b=45),
)
summary = f"{len(rows)} records; total revenue ${sum(totals.values())}."
return figure, summary
app = Dash(__name__)
app.layout = html.Main([
html.H1("Regional sales"),
html.Label("Regions", htmlFor="regions"),
dcc.Dropdown(
id="regions", options=["East", "West"],
value=["East", "West"], multi=True, clearable=True,
),
html.P(id="summary", role="status"),
dcc.Graph(id="sales", responsive=True),
], style={"maxWidth": "800px", "margin": "auto", "padding": "16px"})
@app.callback(
Output("sales", "figure"), Output("summary", "children"),
Input("regions", "value"),
)
def update_sales(regions):
return sales_figure(regions)
if __name__ == "__main__":
app.run(debug=False)
Run .venv/bin/python app.py on macOS/Linux or .venv\Scripts\python.exe app.py in Windows PowerShell. Open the local URL printed by Dash, normally http://127.0.0.1:8050. Keep this development server local. The example neither authenticates users nor supplies a production hosting configuration.
Understand the callback contract
The dropdown's value is an input. The callback returns two values in the order declared by its outputs: the figure and the summary text. The calculation is in a separate ordinary function, so it can be tested without clicking the browser. The fixture remains unchanged when a selection changes; each call builds its own totals and figure.
Dash's callback guide explains this input/output relationship. Current components are imported from dash, and the local runner is app.run(). The old dash_core_components/dash_html_components imports and app.run_server() are not the pattern to copy; see the migration guide.
Check the calculation and the empty state
from app import sales_figure
figure, summary = sales_figure(["East", "West"])
assert list(figure.data[0].x) == ["Books", "Games"]
assert list(figure.data[0].y) == [270, 130]
assert summary == "4 records; total revenue $400."
figure, summary = sales_figure(["East"])
assert list(figure.data[0].y) == [120, 80]
assert summary == "2 records; total revenue $200."
figure, summary = sales_figure([])
assert len(figure.data) == 0
assert summary == "No regions selected."
Also clear the selector in the browser, reselect one region, and use the keyboard to change it. Unknown region values are excluded by the calculation. Treat this as a fixed-data demo: a real application must validate its data contract, define missing-value and currency rules, and enforce authorization on the server.
Extend one layer at a time
Before replacing the fixture with a CSV, decide which columns are required and how duplicate records, missing revenue, and invalid region labels should be handled. Do not automatically drop rows or fill revenue with zero without a business rule. Add a table or download only when it helps the reader answer the dashboard's stated question.
Validation scope: callback results, empty/unknown selections, layout endpoints, and callback HTTP responses were tested locally. The development app was launched for browser inspection. This is a teaching application, not a production deployment benchmark. For a related data-preparation lesson, see cleaning data with NumPy and pandas.