"""
KPI dashboard example using panel-material-ui and HoloViews.
Trend indicators for KPI cards, DynamicMap charts that preserve zoom/pan,
Tabulator checkbox selection that cross-filters charts, and a responsive
pmui.Grid layout. Demonstrates pn.indicators.Trend, DynamicMap with trigger
pattern, Tabulator selection-driven filtering, param.DataFrame as single
source of truth, and pmui.Page template with sidebar filters.
Run: panel serve examples/dashboard.py --dev --show
"""
import numpy as np
import pandas as pd
import panel as pn
import panel_material_ui as pmui
import param
from bokeh.models import NumeralTickFormatter
import holoviews as hv
pn.extension("tabulator", throttled=True, defer_load=True, loading_indicator=True)
# ---------------------------------------------------------------------------
# Theme
# ---------------------------------------------------------------------------
THEME_CONFIG = {
"light": {
"palette": {
"primary": {"main": "#1565c0"},
"secondary": {"main": "#26a69a"},
"success": {"main": "#2e7d32"},
"warning": {"main": "#f57c00"},
"error": {"main": "#c62828"},
},
"typography": {
"fontFamily": "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
"h4": {"fontWeight": 600, "letterSpacing": "-0.02em"},
"h6": {"fontWeight": 600},
"body1": {"lineHeight": 1.6},
},
"shape": {"borderRadius": 12},
"components": {
"MuiPaper": {
"styleOverrides": {
"root": {"boxShadow": "0 2px 12px rgba(0,0,0,0.06)"},
},
},
},
},
"dark": {
"palette": {
"primary": {"main": "#5c9ce6"},
"secondary": {"main": "#4db6ac"},
"success": {"main": "#66bb6a"},
"warning": {"main": "#ffa726"},
"error": {"main": "#ef5350"},
},
"typography": {
"fontFamily": "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
"h4": {"fontWeight": 600, "letterSpacing": "-0.02em"},
"h6": {"fontWeight": 600},
"body1": {"lineHeight": 1.6},
},
"shape": {"borderRadius": 12},
},
}
# ---------------------------------------------------------------------------
# Sample data
# ---------------------------------------------------------------------------
REGIONS = ["North", "South", "East", "West"]
PRODUCTS = ["Widget A", "Widget B", "Widget C", "Widget D"]
np.random.seed(42)
def generate_sales_data():
"""Generate 12 months of daily sales data."""
dates = pd.date_range("2025-01-01", periods=365, freq="D")
rows = []
for date in dates:
for region in REGIONS:
for product in PRODUCTS:
base = {"North": 120, "South": 95, "East": 110, "West": 85}[region]
seasonal = 1 + 0.3 * np.sin(2 * np.pi * date.dayofyear / 365)
product_factor = {"Widget A": 1.2, "Widget B": 0.9, "Widget C": 1.0, "Widget D": 0.7}[product]
revenue = base * seasonal * product_factor * (0.8 + 0.4 * np.random.random())
units = int(revenue / (15 + 5 * np.random.random()))
rows.append({
"date": date,
"region": region,
"product": product,
"revenue": round(revenue, 2),
"units": units,
"cost": round(revenue * (0.55 + 0.1 * np.random.random()), 2),
})
return pd.DataFrame(rows)
# ---------------------------------------------------------------------------
# Dashboard
# ---------------------------------------------------------------------------
REGION_COLORS = {"North": "#ff7f0e", "South": "#2ca02c", "East": "#1f77b4", "West": "#d62728"}
class SalesDashboard(pn.viewable.Viewer):
"""KPI dashboard with Tabulator selection-driven chart filtering."""
_base_df = param.DataFrame(doc="""
Full dataset — generated once, never changes.""")
_filtered_df = param.DataFrame(doc="""
Single source of truth — _base_df narrowed by sidebar filters
and Tabulator selection. KPIs, charts, and table all consume this.""")
_trigger = param.Integer(default=0, doc="""
Signal for DynamicMap — increment to refresh charts while preserving zoom.""")
def __init__(self, df=None, **params):
if df is None:
df = generate_sales_data()
params["_base_df"] = df
# KPI Trend indicators — create once, update data via watcher
trend_opts = dict(sizing_mode="stretch_width", height=120, plot_type="area")
self._revenue_trend = pn.indicators.Trend(
name="Revenue", plot_color="#1565c0", **trend_opts,
)
self._units_trend = pn.indicators.Trend(
name="Units Sold", plot_color="#26a69a", **trend_opts,
)
self._margin_trend = pn.indicators.Trend(
name="Gross Margin %", plot_color="#2e7d32", **trend_opts,
)
self._aov_trend = pn.indicators.Trend(
name="Avg Order Value", plot_color="#f57c00", **trend_opts,
)
# Data table — select rows to cross-filter charts
self._table = pn.widgets.Tabulator(
sizing_mode="stretch_width",
layout="fit_columns",
disabled=True,
selectable="checkbox",
theme="materialize",
pagination="remote",
page_size=15,
show_index=False,
formatters={
"Revenue": {"type": "money", "symbol": "$", "precision": 0},
"Cost": {"type": "money", "symbol": "$", "precision": 0},
},
)
self._table.param.watch(self._on_table_selection, "selection")
# Sidebar widgets
self._date_picker = pmui.DateRangeSlider(
start=pd.Timestamp("2025-01-01"),
end=pd.Timestamp("2025-12-31"),
value=(pd.Timestamp("2025-01-01"), pd.Timestamp("2025-12-31")),
sizing_mode="stretch_width",
margin=(5, 16),
)
self._region_filter = pmui.CheckBoxGroup(
value=list(REGIONS),
options=REGIONS,
label="Regions",
)
self._product_filter = pmui.CheckBoxGroup(
value=list(PRODUCTS),
options=PRODUCTS,
label="Products",
)
super().__init__(**params)
# Wire sidebar widgets → _update_all
pn.bind(
self._update_all,
self._date_picker.param.value,
self._region_filter.param.value,
self._product_filter.param.value,
watch=True,
)
self._update_all() # initial render — populates table data
# Wire checkbox widgets as Tabulator filters — must come after
# _update_all so the table has data for add_filter to inspect.
self._table.add_filter(self._region_filter, "Region")
self._table.add_filter(self._product_filter, "Product")
# DynamicMaps created after super — lazily render with current data,
# preserve zoom/pan on subsequent filter changes via _trigger.
curves_dmap = hv.DynamicMap(pn.bind(self._render_curves, self.param._trigger))
cumulative_dmap = hv.DynamicMap(pn.bind(self._render_cumulative, self.param._trigger))
self._chart_pane = pn.pane.HoloViews(
(curves_dmap + cumulative_dmap).opts(shared_axes=False),
sizing_mode="stretch_width",
height=400,
linked_axes=False,
theme="light_minimal",
)
# -- Data --
def _apply_sidebar_filters(self):
"""Apply sidebar filters to _base_df."""
df = self._base_df
start, end = self._date_picker.value
start = pd.Timestamp(start)
end = pd.Timestamp(end)
mask = (
(df["date"] >= start)
& (df["date"] <= end)
& (df["region"].isin(self._region_filter.value))
& (df["product"].isin(self._product_filter.value))
)
return df[mask]
def _apply_table_selection(self, df):
"""Narrow df by Tabulator checkbox selection, if any."""
if self._table.selection and self._table.value is not None:
selected = self._table.selected_dataframe
if selected is not None and not selected.empty:
keys = selected[["Region", "Product"]].rename(
columns={"Region": "region", "Product": "product"},
)
return df.merge(keys, on=["region", "product"])
return df
# -- Chart rendering (DynamicMap callbacks) --
def _render_curves(self, trigger):
"""Return NdOverlay of weekly revenue curves by region."""
df = self._filtered_df
if df is None or df.empty:
return hv.NdOverlay({"(none)": hv.Curve([], kdims=["date"], vdims=["revenue"])}, kdims=["Region"]).opts(
"Curve", responsive=True, height=350,
)
weekly = df.groupby([pd.Grouper(key="date", freq="W"), "region"]).agg(
revenue=("revenue", "sum"),
).reset_index()
region_curves = {}
for region_name, region_df in weekly.groupby("region"):
region_curves[region_name] = hv.Curve(
region_df, kdims=["date"], vdims=["revenue"],
).opts(
color=REGION_COLORS.get(region_name, "#999"),
line_width=2,
)
if not region_curves:
region_curves["(none)"] = hv.Curve([], kdims=["date"], vdims=["revenue"])
return hv.NdOverlay(region_curves, kdims=["Region"]).opts(
"NdOverlay",
legend_position="top_left",
title="Weekly Revenue by Region",
).opts(
"Curve",
responsive=True,
height=350,
tools=["hover", "xwheel_zoom"],
active_tools=["xwheel_zoom"],
default_tools=["reset"],
hover_tooltips=[
("Region", "$name"),
("Week", "@{date}"),
("Revenue", "@revenue{$0,0}"),
],
xlabel="Date",
ylabel="Revenue ($)",
yformatter=NumeralTickFormatter(format="$0,0"),
gridstyle={"grid_line_alpha": 0.3},
show_grid=True,
)
def _render_cumulative(self, trigger):
"""Return cumulative total revenue curve."""
df = self._filtered_df
if df is None or df.empty:
return hv.Curve([], kdims=["date"], vdims=["cumulative_revenue"]).opts(
responsive=True, height=350,
)
weekly = df.groupby(pd.Grouper(key="date", freq="W")).agg(
revenue=("revenue", "sum"),
).reset_index().sort_values("date")
weekly["cumulative_revenue"] = weekly["revenue"].cumsum()
return hv.Curve(
weekly, kdims=["date"], vdims=["cumulative_revenue"],
).opts(
responsive=True,
height=350,
title="Cumulative Revenue",
color="#1565c0",
line_width=2.5,
tools=["hover", "xwheel_zoom"],
active_tools=["xwheel_zoom"],
default_tools=["reset"],
hover_mode="vline",
hover_tooltips=[
("Date", "@{date}"),
("Cumulative", "@{cumulative_revenue}{$0,0}"),
],
xlabel="Date",
ylabel="Cumulative Revenue ($)",
yformatter=NumeralTickFormatter(format="$0,0"),
gridstyle={"grid_line_alpha": 0.3},
show_grid=True,
)
# -- Updates --
def _on_table_selection(self, event):
"""Re-render charts when table selection changes."""
sidebar_df = self._apply_sidebar_filters()
self._filtered_df = self._apply_table_selection(sidebar_df)
self._trigger += 1
def _update_all(self, *args):
sidebar_df = self._apply_sidebar_filters()
self._filtered_df = self._apply_table_selection(sidebar_df)
with pn.io.hold():
self._update_kpis(self._filtered_df)
self._update_table()
self._trigger += 1 # signal DynamicMaps to re-render
def _update_kpis(self, df):
monthly = df.groupby(df["date"].dt.to_period("M")).agg(
revenue=("revenue", "sum"),
units=("units", "sum"),
cost=("cost", "sum"),
).reset_index()
monthly["date"] = monthly["date"].dt.to_timestamp()
monthly["margin"] = ((monthly["revenue"] - monthly["cost"]) / monthly["revenue"] * 100).round(1)
monthly["aov"] = (monthly["revenue"] / monthly["units"]).round(2)
self._revenue_trend.data = {"x": monthly["date"], "y": monthly["revenue"]}
self._revenue_trend.value = int(df["revenue"].sum())
self._units_trend.data = {"x": monthly["date"], "y": monthly["units"]}
self._units_trend.value = int(df["units"].sum())
total_rev = df["revenue"].sum()
total_cost = df["cost"].sum()
margin_pct = ((total_rev - total_cost) / total_rev * 100) if total_rev else 0
self._margin_trend.data = {"x": monthly["date"], "y": monthly["margin"]}
self._margin_trend.value = round(margin_pct, 1)
aov = total_rev / len(df) if len(df) else 0
self._aov_trend.data = {"x": monthly["date"], "y": monthly["aov"]}
self._aov_trend.value = round(aov, 2)
def _update_table(self):
"""Recompute summary for current date range (all regions/products).
Region and Product filtering is handled by Tabulator's add_filter."""
df = self._base_df
start, end = self._date_picker.value
start, end = pd.Timestamp(start), pd.Timestamp(end)
df = df[(df["date"] >= start) & (df["date"] <= end)]
summary = (
df.groupby(["region", "product"])
.agg(revenue=("revenue", "sum"), units=("units", "sum"), cost=("cost", "sum"))
.reset_index()
)
summary["margin"] = ((summary["revenue"] - summary["cost"]) / summary["revenue"] * 100).round(1)
summary["revenue"] = summary["revenue"].round(0)
summary["cost"] = summary["cost"].round(0)
summary.columns = ["Region", "Product", "Revenue", "Units", "Cost", "Margin %"]
self._table.value = summary
# -- Layout --
def __panel__(self):
kpi_row = pmui.Grid(
pmui.Grid(
pmui.Paper(self._revenue_trend, sx={"p": 1.5}, sizing_mode="stretch_width"),
size={"xs": 12, "sm": 6, "md": 3},
),
pmui.Grid(
pmui.Paper(self._units_trend, sx={"p": 1.5}, sizing_mode="stretch_width"),
size={"xs": 12, "sm": 6, "md": 3},
),
pmui.Grid(
pmui.Paper(self._margin_trend, sx={"p": 1.5}, sizing_mode="stretch_width"),
size={"xs": 12, "sm": 6, "md": 3},
),
pmui.Grid(
pmui.Paper(self._aov_trend, sx={"p": 1.5}, sizing_mode="stretch_width"),
size={"xs": 12, "sm": 6, "md": 3},
),
container=True,
spacing=2,
sizing_mode="stretch_width",
)
charts_section = pmui.Paper(
self._chart_pane,
sx={"p": 2},
sizing_mode="stretch_width",
)
table_section = pmui.Paper(
pmui.Column(
pmui.Typography("Sales summary", variant="h6", sx={"mb": 1}),
pmui.Typography(
"Select rows to filter the charts above.",
sx={"color": "text.secondary", "fontSize": 13, "mb": 1},
),
self._table,
sizing_mode="stretch_width",
),
sx={"p": 2},
sizing_mode="stretch_width",
)
main_content = pmui.Column(
kpi_row,
charts_section,
table_section,
sizing_mode="stretch_width",
margin=(10, 0),
)
if pn.state.served:
return pmui.Page(
title="Sales Dashboard",
sidebar=[
pmui.Typography("Filters", variant="h6", sx={"mb": 1}),
self._date_picker,
pn.layout.Divider(margin=(16, 0)),
self._region_filter,
pn.layout.Divider(margin=(16, 0)),
self._product_filter,
],
sidebar_width=260,
theme_config=THEME_CONFIG,
main=[
pmui.Container(
main_content,
width_option="xl",
)
],
)
return main_content
SalesDashboard().servable()