# How to Build an Interactive Dashboard Using Python and Streamlit

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](https://github.com/Ashakalavagunta/sales-analytics-dashboard)) 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)](https://pranaavkarthik.hashnode.dev/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.

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

Activate it. On Windows:

```bash
venv\Scripts\activate
```

On macOS or Linux:

```bash
source venv/bin/activate
```

Now install the three libraries:

```bash
pip install streamlit pandas plotly
```

Check that Streamlit works:

```bash
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.

```python
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):

```text
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.

```python
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:

```bash
streamlit run app.py
```

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

## How the code works

### Loading data with caching

```python
@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.

### Sidebar filters

```python
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:

```python
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

```python
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

```python
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

```python
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)](https://pranaavkarthik.hashnode.dev/deploy-streamlit-app-community-cloud) 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)](https://pranaavkarthik.hashnode.dev/deploy-streamlit-app-community-cloud). For cleaning messy data before it reaches a dashboard, see [Pandas for Beginners: Cleaning and Analysing Real-World Data](https://pranaavkarthik.hashnode.dev/pandas-for-beginners-cleaning-analysing-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](https://github.com/Pranaav717). To see a much bigger dashboard built with the same tools, read through [Asha Kalavagunta's Sales Analytics Dashboard](https://github.com/Ashakalavagunta/sales-analytics-dashboard).
