Skip to main content

How to Build an Interactive Dashboard Using Python and Streamlit

Updated 9 min readMarkdown

Most Python tutorials end with a chart in a Jupyter notebook. That is fine for you, but nobody else can click around in it. A dashboard fixes that. You get filters, headline numbers and charts on one page, and anyone with the link can use it.

Streamlit lets you build that page with plain Python. No HTML, no JavaScript. In this tutorial you will build a small sales dashboard from scratch: a CSV file, a few KPI cards, sidebar filters and three Plotly charts. It is about 60 lines of code, and every line is shown below.

What Streamlit actually does

Streamlit is an open-source Python library that turns a script into a web app. You write top-to-bottom Python, call functions like st.title() or st.metric(), and each call puts something on the page.

One thing to understand early: Streamlit reruns your whole script every time someone touches a widget. Change a filter, and the script runs again from line 1. That sounds wasteful, but it keeps the mental model simple. It also explains why we cache the data loading step later.

If you want to see what a bigger finished dashboard looks like, there is an open-source Superstore dashboard by Asha Kalavagunta (GitHub repo) with KPI cards, 14 charts and six filters. I went through its code while learning, and the small app here uses the same building blocks at a much smaller scale. What I Learned Studying a Python Sales Analytics Dashboard (Streamlit + Pandas)

What you need

  • Python 3.10 or newer
  • A terminal and a code editor (VS Code is fine)
  • Streamlit 1.32 or newer, Pandas 2.x and Plotly

The code in this post was run with Streamlit 1.67, Pandas 2.3 and Plotly 7. It uses nothing that is new in those versions, apart from one note about chart width that I mention in the charts section.

Step 1: Set up the project

Make a folder and a virtual environment. A virtual environment keeps this project's packages separate from everything else on your machine.

mkdir sales-dashboard
cd sales-dashboard
python -m venv venv

Activate it. On Windows:

venv\Scripts\activate

On macOS or Linux:

source venv/bin/activate

Now install the three libraries:

pip install streamlit pandas plotly

Check that Streamlit works:

streamlit hello

A demo app opens in your browser. If it does, your setup is fine. Press Ctrl+C in the terminal to stop it.

Step 2: Create the sample data

You need a CSV with five columns: Order Date, Region, Category, Sales, Profit. Instead of pasting 60 rows into this post, here is a short script that generates the file. Save it as make_data.py and run python make_data.py once.

import csv, random
random.seed(7)
regions=["East","West","Central","South"]
cats={"Furniture":(120,500,-0.05,0.18),"Technology":(200,900,0.05,0.30),"Office Supplies":(20,200,0.02,0.25)}
import datetime
rows=[]
d=datetime.date(2023,1,1)
for i in range(60):
    od=datetime.date(2023,1,1)+datetime.timedelta(days=i*12+random.randint(0,6))
    c=random.choice(list(cats)); lo,hi,pl,ph=cats[c]
    s=round(random.uniform(lo,hi),2)
    rows.append([od.isoformat(),random.choice(regions),c,s,round(s*random.uniform(pl,ph),2)])
with open("sales.csv","w",newline="") as f:
    w=csv.writer(f); w.writerow(["Order Date","Region","Category","Sales","Profit"]); w.writerows(rows)

You now have sales.csv. The first rows look like this (your numbers will match mine because of the fixed random seed):

Order Date,Region,Category,Sales,Profit
2023-01-03,East,Furniture,270.03,-9.0
2023-01-17,East,Furniture,258.96,41.23

The data is made up, so don't read anything into the numbers. If you have a real CSV with the same five columns, use that instead. The app works the same way.

Step 3: Write the app

Create app.py in the same folder. Here is the full file. The sections after it explain each part.

import pandas as pd
import plotly.express as px
import streamlit as st

st.set_page_config(page_title="Sales Dashboard", layout="wide")
st.title("Sales Dashboard")

@st.cache_data
def load_data(path: str) -> pd.DataFrame:
    df = pd.read_csv(path, parse_dates=["Order Date"])
    df["Month"] = df["Order Date"].dt.to_period("M").dt.to_timestamp()
    return df

df = load_data("sales.csv")

# Sidebar filters
st.sidebar.header("Filters")
regions = st.sidebar.multiselect(
    "Region", options=sorted(df["Region"].unique()), default=sorted(df["Region"].unique())
)
categories = st.sidebar.multiselect(
    "Category", options=sorted(df["Category"].unique()), default=sorted(df["Category"].unique())
)

filtered = df[df["Region"].isin(regions) & df["Category"].isin(categories)]

if filtered.empty:
    st.warning("No data for these filters. Pick at least one region and one category.")
    st.stop()

# KPI cards
total_sales = filtered["Sales"].sum()
total_profit = filtered["Profit"].sum()
margin = total_profit / total_sales * 100

c1, c2, c3 = st.columns(3)
c1.metric("Total Sales", f"${total_sales:,.0f}")
c2.metric("Total Profit", f"${total_profit:,.0f}")
c3.metric("Profit Margin", f"{margin:.1f}%")

# Charts
monthly = filtered.groupby("Month", as_index=False)[["Sales", "Profit"]].sum()
fig_line = px.line(monthly, x="Month", y=["Sales", "Profit"], markers=True,
                   title="Monthly Sales and Profit")
st.plotly_chart(fig_line)

left, right = st.columns(2)
by_region = filtered.groupby("Region", as_index=False)["Sales"].sum()
left.plotly_chart(px.bar(by_region, x="Region", y="Sales", title="Sales by Region"))
by_cat = filtered.groupby("Category", as_index=False)["Profit"].sum()
right.plotly_chart(px.pie(by_cat, names="Category", values="Profit", hole=0.4,
                          title="Profit by Category"))

with st.expander("See the data"):
    st.dataframe(filtered)

Run it:

streamlit run app.py

Your browser opens at http://localhost:8501.

How the code works

Loading data with caching

@st.cache_data
def load_data(path: str) -> pd.DataFrame:
    df = pd.read_csv(path, parse_dates=["Order Date"])
    df["Month"] = df["Order Date"].dt.to_period("M").dt.to_timestamp()
    return df

Remember that Streamlit reruns the script on every interaction. Without caching, it would re-read the CSV each time you click a filter. @st.cache_data stores the returned DataFrame, so the function body only runs the first time (or when the file path argument changes).

parse_dates=["Order Date"] tells Pandas to read that column as real dates, not text. That matters because the next line, .dt.to_period("M").dt.to_timestamp(), rounds each date down to the first day of its month. We use that Month column to group sales by month later.

One caveat: if you edit sales.csv while the app is running, the cache won't notice, because the function argument (the path) hasn't changed. Press C in the app and choose "Clear cache", or restart it.

regions = st.sidebar.multiselect(
    "Region", options=sorted(df["Region"].unique()), default=sorted(df["Region"].unique())
)

st.sidebar.multiselect draws a dropdown in the left panel where people can pick several values. We fill the options from the data itself, so if a new region appears in the CSV, the filter picks it up without code changes. Setting default to all options means the dashboard starts unfiltered.

Then we filter the DataFrame:

filtered = df[df["Region"].isin(regions) & df["Category"].isin(categories)]

.isin() keeps rows whose value is in the selected list. The & joins two conditions, and each one needs to stay in its own comparison. Everything below this line uses filtered, not df, so every number and chart reacts to the filters.

Handling the empty case

if filtered.empty:
    st.warning("No data for these filters. Pick at least one region and one category.")
    st.stop()

If someone clears every option, filtered has zero rows, and the profit margin line would divide by zero. st.stop() ends the script run right there, so nothing below it executes. Try it: remove all regions in the sidebar and you get the warning instead of a crash.

KPI cards with st.metric

c1, c2, c3 = st.columns(3)
c1.metric("Total Sales", f"${total_sales:,.0f}")

st.columns(3) splits the page into three side-by-side containers. st.metric(label, value) shows a big number with a small label above it. The f-string format :,.0f adds thousand separators and drops the decimals. st.metric also accepts a delta argument if you want to show a change, such as vs last month, but we skip it here to keep the app simple.

Charts with Plotly Express

monthly = filtered.groupby("Month", as_index=False)[["Sales", "Profit"]].sum()
fig_line = px.line(monthly, x="Month", y=["Sales", "Profit"], markers=True,
                   title="Monthly Sales and Profit")
st.plotly_chart(fig_line)

The pattern is always the same: summarise with groupby, build a figure with px.something, then hand it to st.plotly_chart. Passing a list to y draws one line per column. as_index=False keeps Month as a normal column, which is what Plotly Express expects.

The bar chart and donut chart follow the same pattern. A donut is just px.pie with hole=0.4.

A note on chart width. In Streamlit 1.42 and later, Plotly charts stretch to the container by default, which is what this code relies on. If you are on 1.32 to 1.41, add use_container_width=True inside each st.plotly_chart() call or the charts will keep Plotly's default width. In the newest releases use_container_width is deprecated in favour of width="stretch", so pick based on your version. The simplest fix is to upgrade Streamlit.

Common beginner mistakes

Forgetting the script reruns. If you put counter = 0 at the top and expect it to grow with clicks, it won't. Every rerun resets it. For values that must survive reruns, look at st.session_state.

Running python app.py. This does not start the web server. You'll get a few warnings and no dashboard. Always use streamlit run app.py.

Wrong working directory. pd.read_csv("sales.csv") looks in the folder you launched the command from. If you see FileNotFoundError, cd into the project folder first. How to Deploy a Python Streamlit Project Online (Free, Step by Step) covers a more robust way to build paths.

Filtering the original DataFrame. If you chart df instead of filtered, the widgets will appear to do nothing. Check which variable each chart uses.

Column name typos. "Order date" and "Order Date" are different to Pandas. A KeyError mentioning a column name almost always means a spelling or case mismatch.

Caching something that should change. @st.cache_data is for loading and heavy calculations. Don't wrap your filter logic in it unless you pass the filter values in as arguments.

Where to go next

Once this runs, try one change at a time:

  1. Add a date range filter with st.sidebar.date_input.
  2. Add a fourth KPI, like average order value.
  3. Swap the donut for a px.scatter of Sales vs Profit and see which orders lose money.
  4. Load your own CSV with st.file_uploader.

When you are ready to put it online, push the folder to GitHub and follow How to Deploy a Python Streamlit Project Online (Free, Step by Step). For cleaning messy data before it reaches a dashboard, see Pandas for Beginners: Cleaning and Analysing Real-World Data.

Wrap up

You now have a working dashboard: cached data loading, multiselect filters, KPI cards and three interactive charts, all in one short Python file. The Streamlit docs at docs.streamlit.io are worth bookmarking, since every widget there has a runnable example.

If this was useful, follow the blog for the next posts in this series, and have a look at my GitHub profile. To see a much bigger dashboard built with the same tools, read through Asha Kalavagunta's Sales Analytics Dashboard.

No comments yet. Be the first to comment.

More from this blog