71803418e5
Shelly device management app with mDNS/subnet discovery, inventory, configuration, and mass operations for Gen1/Gen2+ devices. Includes .gitignore excluding runtime data (device DB, user config), AI conversation history, build artifacts, and common Python/OS patterns.
427 lines
13 KiB
Python
427 lines
13 KiB
Python
"""
|
|
Metrics Dashboard Template
|
|
|
|
A comprehensive metrics dashboard demonstrating:
|
|
- Time series visualization with Altair (line, area, bar, point charts)
|
|
- Metric cards with chart/table toggle and popover filters
|
|
- Time range filtering (1M, 6M, 1Y, QTD, YTD, All)
|
|
- Line options (Daily, 7-day MA)
|
|
|
|
This template uses synthetic data. Replace the generate_*_data() functions
|
|
with your own data sources (e.g., Snowflake queries, APIs, etc.)
|
|
"""
|
|
|
|
from datetime import date, timedelta
|
|
import numpy as np
|
|
import pandas as pd
|
|
import streamlit as st
|
|
import altair as alt
|
|
|
|
st.set_page_config(
|
|
page_title="Metrics Dashboard",
|
|
page_icon=":material/monitoring:",
|
|
layout="wide",
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Constants
|
|
# =============================================================================
|
|
|
|
TIME_RANGES = ["1M", "6M", "1Y", "QTD", "YTD", "All"]
|
|
CHART_HEIGHT = 300
|
|
|
|
|
|
# =============================================================================
|
|
# Synthetic Data Generation (Replace with your data source)
|
|
# =============================================================================
|
|
|
|
|
|
def generate_metric_data(
|
|
metric_name: str,
|
|
start_date: date,
|
|
end_date: date,
|
|
base_value: float = 1000,
|
|
growth_rate: float = 0.001,
|
|
noise_factor: float = 0.1,
|
|
) -> pd.DataFrame:
|
|
"""Generate synthetic time series data for a metric.
|
|
|
|
Replace this function with your actual data source, e.g.:
|
|
- Snowflake query
|
|
- API call
|
|
- Database query
|
|
"""
|
|
np.random.seed(hash(metric_name) % 2**32)
|
|
|
|
dates = pd.date_range(start=start_date, end=end_date, freq="D")
|
|
n_days = len(dates)
|
|
|
|
# Base trend with growth
|
|
trend = base_value * (1 + growth_rate) ** np.arange(n_days)
|
|
|
|
# Add weekly seasonality (lower on weekends)
|
|
day_of_week = dates.dayofweek
|
|
seasonality = np.where(day_of_week >= 5, 0.7, 1.0)
|
|
trend = trend * seasonality
|
|
|
|
# Add noise
|
|
noise = np.random.normal(1, noise_factor, n_days)
|
|
values = trend * noise
|
|
|
|
# Calculate rolling averages
|
|
df = pd.DataFrame({
|
|
"ds": dates,
|
|
"daily_value": values,
|
|
})
|
|
df["value_7d_ma"] = df["daily_value"].rolling(7, min_periods=1).mean()
|
|
|
|
return df
|
|
|
|
|
|
@st.cache_data(ttl=3600)
|
|
def load_all_metrics() -> dict[str, pd.DataFrame]:
|
|
"""Load all metrics data. Replace with your data loading logic."""
|
|
end_date = date.today() - timedelta(days=1)
|
|
start_date = end_date - timedelta(days=730) # 2 years of data
|
|
|
|
return {
|
|
"users": generate_metric_data("users", start_date, end_date, base_value=5000, growth_rate=0.002),
|
|
"sessions": generate_metric_data("sessions", start_date, end_date, base_value=15000, growth_rate=0.003),
|
|
"revenue": generate_metric_data("revenue", start_date, end_date, base_value=50000, growth_rate=0.001),
|
|
"conversions": generate_metric_data("conversions", start_date, end_date, base_value=500, growth_rate=0.0015),
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# Chart Utilities
|
|
# =============================================================================
|
|
|
|
|
|
def filter_by_time_range(df: pd.DataFrame, x_col: str, time_range: str) -> pd.DataFrame:
|
|
"""Filter dataframe by time range."""
|
|
if time_range == "All" or df.empty:
|
|
return df
|
|
|
|
df = df.copy()
|
|
df[x_col] = pd.to_datetime(df[x_col])
|
|
max_date = df[x_col].max()
|
|
|
|
if time_range == "1M":
|
|
min_date = max_date - timedelta(days=30)
|
|
elif time_range == "6M":
|
|
min_date = max_date - timedelta(days=180)
|
|
elif time_range == "1Y":
|
|
min_date = max_date - timedelta(days=365)
|
|
elif time_range == "QTD":
|
|
quarter_month = ((max_date.month - 1) // 3) * 3 + 1
|
|
min_date = pd.Timestamp(date(max_date.year, quarter_month, 1))
|
|
elif time_range == "YTD":
|
|
min_date = pd.Timestamp(date(max_date.year, 1, 1))
|
|
else:
|
|
return df
|
|
|
|
return df[df[x_col] >= min_date]
|
|
|
|
|
|
def render_line_chart(
|
|
df: pd.DataFrame,
|
|
x_col: str,
|
|
y_cols: list[str],
|
|
labels: list[str],
|
|
height: int = CHART_HEIGHT,
|
|
) -> alt.Chart:
|
|
"""Render a multi-line chart."""
|
|
# Melt for Altair
|
|
melted = df.melt(
|
|
id_vars=[x_col],
|
|
value_vars=y_cols,
|
|
var_name="series",
|
|
value_name="value",
|
|
)
|
|
|
|
# Map to labels
|
|
label_map = dict(zip(y_cols, labels))
|
|
melted["series"] = melted["series"].map(label_map)
|
|
|
|
chart = (
|
|
alt.Chart(melted)
|
|
.mark_line()
|
|
.encode(
|
|
x=alt.X(f"{x_col}:T", title=None),
|
|
y=alt.Y("value:Q", title=None, scale=alt.Scale(zero=False)),
|
|
color=alt.Color("series:N", title=None, legend=alt.Legend(orient="bottom")),
|
|
strokeDash=alt.condition(
|
|
alt.datum.series == "7-day MA",
|
|
alt.value([5, 5]),
|
|
alt.value([0]),
|
|
),
|
|
tooltip=[
|
|
alt.Tooltip(f"{x_col}:T", title="Date", format="%Y-%m-%d"),
|
|
alt.Tooltip("series:N", title="Series"),
|
|
alt.Tooltip("value:Q", title="Value", format=",.0f"),
|
|
],
|
|
)
|
|
.properties(height=height)
|
|
)
|
|
|
|
return chart
|
|
|
|
|
|
def render_area_chart(
|
|
df: pd.DataFrame,
|
|
x_col: str,
|
|
y_cols: list[str],
|
|
labels: list[str],
|
|
height: int = CHART_HEIGHT,
|
|
) -> alt.Chart:
|
|
"""Render a stacked area chart."""
|
|
melted = df.melt(
|
|
id_vars=[x_col],
|
|
value_vars=y_cols,
|
|
var_name="series",
|
|
value_name="value",
|
|
)
|
|
label_map = dict(zip(y_cols, labels))
|
|
melted["series"] = melted["series"].map(label_map)
|
|
|
|
chart = (
|
|
alt.Chart(melted)
|
|
.mark_area(opacity=0.6, line=True)
|
|
.encode(
|
|
x=alt.X(f"{x_col}:T", title=None),
|
|
y=alt.Y("value:Q", title=None, scale=alt.Scale(zero=False)),
|
|
color=alt.Color("series:N", title=None, legend=alt.Legend(orient="bottom")),
|
|
tooltip=[
|
|
alt.Tooltip(f"{x_col}:T", title="Date", format="%Y-%m-%d"),
|
|
alt.Tooltip("series:N", title="Series"),
|
|
alt.Tooltip("value:Q", title="Value", format=",.0f"),
|
|
],
|
|
)
|
|
.properties(height=height)
|
|
)
|
|
return chart
|
|
|
|
|
|
def render_bar_chart(
|
|
df: pd.DataFrame,
|
|
x_col: str,
|
|
y_cols: list[str],
|
|
labels: list[str],
|
|
height: int = CHART_HEIGHT,
|
|
) -> alt.Chart:
|
|
"""Render a bar chart (weekly aggregation for readability)."""
|
|
df = df.copy()
|
|
df[x_col] = pd.to_datetime(df[x_col])
|
|
df["week"] = df[x_col].dt.to_period("W").dt.start_time
|
|
|
|
# Aggregate by week
|
|
agg_df = df.groupby("week")[y_cols].mean().reset_index()
|
|
|
|
melted = agg_df.melt(
|
|
id_vars=["week"],
|
|
value_vars=y_cols,
|
|
var_name="series",
|
|
value_name="value",
|
|
)
|
|
label_map = dict(zip(y_cols, labels))
|
|
melted["series"] = melted["series"].map(label_map)
|
|
|
|
chart = (
|
|
alt.Chart(melted)
|
|
.mark_bar(opacity=0.8)
|
|
.encode(
|
|
x=alt.X("week:T", title=None),
|
|
y=alt.Y("value:Q", title=None, scale=alt.Scale(zero=False)),
|
|
color=alt.Color("series:N", title=None, legend=alt.Legend(orient="bottom")),
|
|
xOffset="series:N",
|
|
tooltip=[
|
|
alt.Tooltip("week:T", title="Week", format="%Y-%m-%d"),
|
|
alt.Tooltip("series:N", title="Series"),
|
|
alt.Tooltip("value:Q", title="Value", format=",.0f"),
|
|
],
|
|
)
|
|
.properties(height=height)
|
|
)
|
|
return chart
|
|
|
|
|
|
def render_point_chart(
|
|
df: pd.DataFrame,
|
|
x_col: str,
|
|
y_cols: list[str],
|
|
labels: list[str],
|
|
height: int = CHART_HEIGHT,
|
|
) -> alt.Chart:
|
|
"""Render a scatter/point chart with trend line."""
|
|
melted = df.melt(
|
|
id_vars=[x_col],
|
|
value_vars=y_cols,
|
|
var_name="series",
|
|
value_name="value",
|
|
)
|
|
label_map = dict(zip(y_cols, labels))
|
|
melted["series"] = melted["series"].map(label_map)
|
|
|
|
points = (
|
|
alt.Chart(melted)
|
|
.mark_point(opacity=0.5, size=20)
|
|
.encode(
|
|
x=alt.X(f"{x_col}:T", title=None),
|
|
y=alt.Y("value:Q", title=None, scale=alt.Scale(zero=False)),
|
|
color=alt.Color("series:N", title=None, legend=alt.Legend(orient="bottom")),
|
|
tooltip=[
|
|
alt.Tooltip(f"{x_col}:T", title="Date", format="%Y-%m-%d"),
|
|
alt.Tooltip("series:N", title="Series"),
|
|
alt.Tooltip("value:Q", title="Value", format=",.0f"),
|
|
],
|
|
)
|
|
)
|
|
|
|
# Add trend line for 7-day MA only
|
|
trend = (
|
|
alt.Chart(melted[melted["series"] == "7-day MA"])
|
|
.mark_line(strokeDash=[5, 5], strokeWidth=2)
|
|
.encode(
|
|
x=alt.X(f"{x_col}:T"),
|
|
y=alt.Y("value:Q"),
|
|
color=alt.Color("series:N"),
|
|
)
|
|
)
|
|
|
|
return (points + trend).properties(height=height)
|
|
|
|
|
|
# =============================================================================
|
|
# Metric Card Component
|
|
# =============================================================================
|
|
|
|
|
|
def metric_card(
|
|
title: str,
|
|
df: pd.DataFrame,
|
|
key_prefix: str,
|
|
chart_type: str = "line",
|
|
):
|
|
"""Display a metric card with chart/table toggle and popover filters.
|
|
|
|
Args:
|
|
title: Card title
|
|
df: DataFrame with ds, daily_value, value_7d_ma columns
|
|
key_prefix: Unique prefix for widget keys
|
|
chart_type: One of "line", "area", "bar", "point"
|
|
"""
|
|
chart_renderers = {
|
|
"line": render_line_chart,
|
|
"area": render_area_chart,
|
|
"bar": render_bar_chart,
|
|
"point": render_point_chart,
|
|
}
|
|
render_chart = chart_renderers.get(chart_type, render_line_chart)
|
|
|
|
with st.container(border=True):
|
|
# Header row with title, view toggle, and filters
|
|
with st.container(
|
|
horizontal=True,
|
|
horizontal_alignment="distribute",
|
|
vertical_alignment="center",
|
|
):
|
|
st.markdown(f"**{title}**")
|
|
|
|
view_mode = st.segmented_control(
|
|
"View",
|
|
options=[":material/show_chart:", ":material/table:"],
|
|
default=":material/show_chart:",
|
|
key=f"{key_prefix}_view",
|
|
label_visibility="collapsed",
|
|
)
|
|
|
|
with st.popover("Filters", type="tertiary"):
|
|
line_options = st.pills(
|
|
"Lines",
|
|
options=["Daily", "7-day MA"],
|
|
default=["Daily", "7-day MA"],
|
|
selection_mode="multi",
|
|
key=f"{key_prefix}_lines",
|
|
)
|
|
time_range = st.segmented_control(
|
|
"Time range",
|
|
options=TIME_RANGES,
|
|
default="All",
|
|
key=f"{key_prefix}_time",
|
|
)
|
|
|
|
# Apply filters
|
|
line_options = line_options or ["7-day MA"]
|
|
filtered_df = filter_by_time_range(df, "ds", time_range)
|
|
|
|
# Determine which columns to show
|
|
y_cols = []
|
|
labels = []
|
|
if "Daily" in line_options:
|
|
y_cols.append("daily_value")
|
|
labels.append("Daily")
|
|
if "7-day MA" in line_options:
|
|
y_cols.append("value_7d_ma")
|
|
labels.append("7-day MA")
|
|
|
|
# Render view
|
|
if "table" in (view_mode or ""):
|
|
st.dataframe(
|
|
filtered_df,
|
|
height=CHART_HEIGHT,
|
|
hide_index=True,
|
|
)
|
|
else:
|
|
if y_cols:
|
|
st.altair_chart(
|
|
render_chart(filtered_df, "ds", y_cols, labels),
|
|
)
|
|
else:
|
|
st.info("Select at least one line option.")
|
|
|
|
|
|
# =============================================================================
|
|
# Page Header Component
|
|
# =============================================================================
|
|
|
|
|
|
def render_page_header(title: str):
|
|
"""Render page header with title and reset button."""
|
|
with st.container(
|
|
horizontal=True, horizontal_alignment="distribute", vertical_alignment="center"
|
|
):
|
|
st.markdown(title)
|
|
if st.button(":material/restart_alt: Reset", type="tertiary"):
|
|
st.session_state.clear()
|
|
st.rerun()
|
|
|
|
|
|
# =============================================================================
|
|
# Page Layout
|
|
# =============================================================================
|
|
|
|
# Load data (cached)
|
|
metrics_data = load_all_metrics()
|
|
|
|
# Page header
|
|
render_page_header("# :material/monitoring: Metrics Dashboard")
|
|
|
|
# Row 1: Users and Sessions
|
|
row1 = st.columns(2)
|
|
|
|
with row1[0]:
|
|
metric_card("Active Users", metrics_data["users"], "users", chart_type="line")
|
|
|
|
with row1[1]:
|
|
metric_card("Sessions", metrics_data["sessions"], "sessions", chart_type="area")
|
|
|
|
# Row 2: Revenue and Conversions
|
|
row2 = st.columns(2)
|
|
|
|
with row2[0]:
|
|
metric_card("Revenue", metrics_data["revenue"], "revenue", chart_type="bar")
|
|
|
|
with row2[1]:
|
|
metric_card("Conversions", metrics_data["conversions"], "conversions", chart_type="point")
|