"""
KPI Superstore Dashboard - Vizro version.
Data from data/orders.csv (pandas only). Generate with: python export_orders_from_hyper.py
"""
from pathlib import Path
import pandas as pd
import plotly.graph_objects as go
from dash import html, dcc
import dash_bootstrap_components as dbc
from vizro import Vizro
import vizro.models as vm
from vizro.models.types import capture
from vizro.managers import data_manager
# ---------------------------------------------------------------------------
# Data loading - pandas only
# ---------------------------------------------------------------------------
ORDERS_CSV = Path("orders.csv")
def load_orders_df():
if not ORDERS_CSV.exists():
raise FileNotFoundError(
f"Data not found: {ORDERS_CSV}. Run export_orders_from_hyper.py once to create it."
)
df = pd.read_csv(ORDERS_CSV, parse_dates=["Order Date"])
return df.dropna(subset=["Order Date"])
data_manager["orders"] = load_orders_df()
# ---------------------------------------------------------------------------
# Date period and KPI logic (same as app.py)
# ---------------------------------------------------------------------------
def get_period_bounds(date_view, ref_date):
ref = pd.Timestamp(ref_date)
if date_view == "YTD":
cur_start = ref.replace(month=1, day=1)
cur_end = ref
prior_end = cur_start - pd.Timedelta(days=1)
prior_start = prior_end.replace(month=1, day=1)
elif date_view == "QTD":
q = (ref.month - 1) // 3 + 1
cur_start = ref.replace(month=1 + (q - 1) * 3, day=1)
cur_end = ref
prior_end = cur_start - pd.Timedelta(days=1)
prior_start = prior_end.replace(month=1 + (q - 1) * 3, day=1)
else:
cur_start = ref.replace(day=1)
cur_end = ref
prior_end = cur_start - pd.Timedelta(days=1)
prior_start = prior_end.replace(day=1)
return (cur_start, cur_end), (prior_start, prior_end)
def compute_kpis(df, date_view):
if df.empty:
return None
ref_date = df["Order Date"].max()
(cur_start, cur_end), (prior_start, prior_end) = get_period_bounds(date_view, ref_date)
cur = df[(df["Order Date"] >= cur_start) & (df["Order Date"] <= cur_end)]
prior = df[(df["Order Date"] >= prior_start) & (df["Order Date"] <= prior_end)]
if cur.empty:
return None
orders_cur = cur["Order ID"].nunique() if "Order ID" in cur.columns else len(cur)
orders_prior = prior["Order ID"].nunique() if "Order ID" in prior.columns else len(prior)
def pct(now, prev):
return (now - prev) / prev * 100 if prev else 0.0
return {
"ref_date": ref_date,
"sales_cur": cur["Sales"].sum(), "sales_prior": prior["Sales"].sum(),
"sales_pct": pct(cur["Sales"].sum(), prior["Sales"].sum()),
"orders_cur": orders_cur, "orders_prior": orders_prior,
"orders_pct": pct(orders_cur, orders_prior),
"profit_cur": cur["Profit"].sum(), "profit_prior": prior["Profit"].sum(),
"profit_pct": pct(cur["Profit"].sum(), prior["Profit"].sum()),
"qty_cur": cur["Quantity"].sum(), "qty_prior": prior["Quantity"].sum(),
"qty_pct": pct(cur["Quantity"].sum(), prior["Quantity"].sum()),
"cur_df": cur, "prior_df": prior,
}
def build_chart_series(cur_df, prior_df, date_view, value_col, agg="sum"):
if cur_df.empty:
return pd.Series(dtype=float), pd.Series(dtype=float)
if date_view == "MTD":
cur_df = cur_df.copy()
cur_df["_period"] = cur_df["Order Date"].dt.day
prior_df = prior_df.copy()
prior_df["_period"] = prior_df["Order Date"].dt.day
else:
cur_df = cur_df.copy()
cur_df["_period"] = cur_df["Order Date"].dt.to_period("M").astype(str)
prior_df = prior_df.copy()
prior_df["_period"] = prior_df["Order Date"].dt.to_period("M").astype(str)
if agg == "sum":
cur_s = cur_df.groupby("_period", sort=True)[value_col].sum()
prior_s = prior_df.groupby("_period", sort=True)[value_col].sum()
else:
cur_s = cur_df.groupby("_period", sort=True)[value_col].nunique()
prior_s = prior_df.groupby("_period", sort=True)[value_col].nunique()
all_periods = sorted(set(cur_s.index) | set(prior_s.index))
return cur_s.reindex(all_periods, fill_value=0), prior_s.reindex(all_periods, fill_value=0)
def make_mini_chart(cur_series, prior_series):
fig = go.Figure()
x = list(range(len(prior_series)))
fig.add_trace(go.Bar(x=x, y=prior_series.values, name="Previous Year", marker_color="lightgray", width=0.4))
fig.add_trace(go.Bar(x=x, y=cur_series.values, name="Current YTD", marker_color="darkgray", width=0.5))
fig.add_trace(go.Scatter(x=x, y=cur_series.values, mode="lines", line=dict(color="black", width=2)))
fig.update_layout(
margin=dict(l=30, r=30, t=20, b=30), showlegend=False, height=180,
paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
xaxis=dict(title=None), yaxis=dict(title=None),
)
fig.update_xaxes(showgrid=False)
fig.update_yaxes(showgrid=True, gridcolor="lightgray")
return fig
def fmt_currency(x):
if abs(x) >= 1e6:
return f"${x/1e6:.1f}M"
if abs(x) >= 1e3:
return f"${x/1e3:.0f}K"
return f"${x:,.0f}"
# ---------------------------------------------------------------------------
# Custom figure: one reactive component that renders the full KPI dashboard
# ---------------------------------------------------------------------------
@capture("figure")
def kpi_dashboard(data_frame: pd.DataFrame, date_view: str = "YTD"):
"""Returns a Dash component with 4 KPI cards and 4 mini charts. Reacts to Filter (data_frame) and Parameter (date_view)."""
period_label = {"YTD": "current YTD", "QTD": "current QTD", "MTD": "current MTD"}.get(date_view, "current YTD")
k = compute_kpis(data_frame, date_view)
if k is None:
empty = html.Div("No data", style={"padding": 20})
return html.Div([empty] * 4, style={"display": "grid", "gridTemplateColumns": "1fr 1fr", "gap": 16})
cur_sales, prior_sales = build_chart_series(k["cur_df"], k["prior_df"], date_view, "Sales")
cur_orders, prior_orders = build_chart_series(k["cur_df"], k["prior_df"], date_view, "Order ID", agg="nunique")
cur_profit, prior_profit = build_chart_series(k["cur_df"], k["prior_df"], date_view, "Profit")
cur_qty, prior_qty = build_chart_series(k["cur_df"], k["prior_df"], date_view, "Quantity")
def pct_badge(pct):
color = "#16a085" if pct >= 0 else "#c0392b"
return html.Span(f"{pct:+.0f}% vs prior year", style={"backgroundColor": color, "color": "white", "padding": "2px 8px", "borderRadius": 4, "fontSize": "0.8rem"})
def card(title, title_color, value, subtitle, pct_el, chart_fig):
return dbc.Card([
dbc.CardHeader(html.Div([
html.Strong(title, style={"color": "#333"}),
html.Hr(style={"margin": "4px 0", "border": "none", "borderTop": f"3px solid {title_color}", "width": 40}),
])),
dbc.CardBody([
html.Div(value, style={"fontSize": "1.8rem", "fontWeight": "bold"}),
html.Div(subtitle, style={"fontSize": "0.75rem", "color": "#888"}),
html.Div(pct_el, style={"marginTop": 4}),
dcc.Graph(figure=chart_fig, config={"displayModeBar": False}, style={"height": 180}),
]),
], style={"boxShadow": "0 1px 3px rgba(0,0,0,0.1)"})
cards = [
card("SALES", "#c0392b", fmt_currency(k["sales_cur"]), period_label, pct_badge(k["sales_pct"]), make_mini_chart(cur_sales, prior_sales)),
card("ORDERS", "#16a085", f"{k['orders_cur']:,.0f}", period_label, pct_badge(k["orders_pct"]), make_mini_chart(cur_orders, prior_orders)),
card("PROFIT", "#c0392b", fmt_currency(k["profit_cur"]), period_label, pct_badge(k["profit_pct"]), make_mini_chart(cur_profit, prior_profit)),
card("QUANTITY", "#16a085", f"{k['qty_cur']:,.0f}", period_label, pct_badge(k["qty_pct"]), make_mini_chart(cur_qty, prior_qty)),
]
return html.Div(cards, style={"display": "grid", "gridTemplateColumns": "1fr 1fr", "gap": 16})
# ---------------------------------------------------------------------------
# Build dashboard
# ---------------------------------------------------------------------------
page = vm.Page(
title="KPI Superstore Dashboard",
components=[
vm.Figure(
id="kpi_dashboard",
figure=kpi_dashboard(data_frame="orders", date_view="YTD"),
),
],
controls=[
vm.Parameter(
targets=["kpi_dashboard.date_view"],
selector=vm.RadioItems(
options=["YTD", "QTD", "MTD"],
value="YTD",
title="Select Date View",
),
),
vm.Filter(column="Region", selector=vm.Dropdown(title="Select Region", multi=True)),
vm.Filter(column="Category", selector=vm.Dropdown(title="Select Category", multi=True)),
vm.Filter(column="Sub-Category", selector=vm.Dropdown(title="Select Sub-Category", multi=True)),
],
layout=vm.Grid(grid=[[0]]),
)
dashboard = vm.Dashboard(pages=[page], title="KPI Superstore Dashboard")
app = Vizro().build(dashboard)