# Colab setup: fetch repo so we can read data
import os
from google.colab import files
= '05_advanced'
MODULE = 'large_food_log.csv'
DATASET = '/content/data-analysis-projects'
BASE_PATH = os.path.join(BASE_PATH, 'notebooks', MODULE)
MODULE_PATH = os.path.join(MODULE_PATH, 'data', DATASET)
DATASET_PATH
try:
print('Attempting to clone repository...')
if not os.path.exists(BASE_PATH):
!git clone https://github.com/ggkuhnle/data-analysis-projects.git
os.chdir(MODULE_PATH)if os.path.exists(DATASET_PATH):
print(f'Dataset found: {DATASET_PATH} β
')
else:
raise FileNotFoundError('Dataset missing after clone.')
except Exception as e:
print(f'Cloning failed: {e}')
print('Falling back to manual upload...')
'data', exist_ok=True)
os.makedirs(= files.upload()
uploaded if DATASET in uploaded:
with open(os.path.join('data', DATASET), 'wb') as f:
f.write(uploaded[DATASET])print(f'Successfully uploaded {DATASET} β
')
else:
raise FileNotFoundError(f'Upload {DATASET} to continue.')
π 5.5 Creating Dashboards
In this notebook youβll build an interactive dashboard for nutrition data with Plotly and ipywidgets, and export a single-file HTML you can share.
You will: - Load and tidy large_food_log.csv
(hippo snacks ππ¦). - Create interactive controls (Nutrient / Meal / Date range). - Build key visuals: trend lines, stacked bars, treemap, rolling averages. - Show KPIs (totals, daily average, unique days). - Export a shareable dashboard.html.
Why dashboards?
Dashboards condense many views into one space so trends pop out fast β perfect for busy nutrition teams.%pip install -q pandas plotly ipywidgets
import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from ipywidgets import interact, widgets, Layout, HBox, VBox
from pathlib import Path
'display.max_columns', 40)
pd.set_option(print('Dashboard environment ready.')
π₯ Load & Tidy
Weβll parse dates, normalise text columns, and add a few helpful features (year / month, day-of-week).
= Path('data') / 'large_food_log.csv'
csv_path = pd.read_csv(csv_path)
df
# Basic cleaning
for col in ['ID','Meal','Nutrient']:
if col in df.columns:
= df[col].astype(str).str.strip()
df[col] 'Amount'] = pd.to_numeric(df['Amount'], errors='coerce')
df['Date'] = pd.to_datetime(df['Date'], errors='coerce')
df[
# Filter invalid rows
= df.dropna(subset=['Amount','Date'])
df = df[df['Amount'] >= 0]
df
# Time features
'Year'] = df['Date'].dt.year
df['Month'] = df['Date'].dt.to_period('M').astype(str)
df['DOW'] = df['Date'].dt.day_name()
df[
display(df.head())print('\nRows:', len(df), '| Date range:', df['Date'].min().date(), 'β', df['Date'].max().date())
π§° Helpers: KPI & Slicer Functions
A couple of small utilities weβll reuse across the dashboard.
def slice_data(df, nutrient=None, meal=None, date_range=None):
= df.copy()
d if nutrient and nutrient != 'All':
= d[d['Nutrient'] == nutrient]
d if meal and meal != 'All':
= d[d['Meal'] == meal]
d if date_range and all(date_range):
= d[(d['Date'] >= date_range[0]) & (d['Date'] <= date_range[1])]
d return d
def kpis(d):
= d['Amount'].sum()
total = d['Date'].nunique()
n_days = total / n_days if n_days else 0.0
daily_avg return {
'Total amount': total,
'Unique days': int(n_days),
'Daily average': daily_avg
}
ποΈ Interactive Controls
Pick a Nutrient, Meal, and a Date range to drive the visuals below.
= ['All'] + sorted(df['Nutrient'].dropna().unique().tolist())
nutrients = ['All'] + sorted(df['Meal'].dropna().unique().tolist())
meals = df['Date'].min().date(), df['Date'].max().date()
start, end
= widgets.Dropdown(options=nutrients, value='All', description='Nutrient:', layout=Layout(width='250px'))
w_nutrient = widgets.Dropdown(options=meals, value='All', description='Meal:', layout=Layout(width='250px'))
w_meal = widgets.DatePicker(description='Start:', value=pd.Timestamp(start).date())
w_start = widgets.DatePicker(description='End:', value=pd.Timestamp(end).date())
w_end
display(HBox([w_nutrient, w_meal, w_start, w_end]))
π KPIs
Quick headline numbers update with your selections.
def render_kpis(nutrient, meal, start_date, end_date):
= slice_data(df, nutrient, meal, (pd.to_datetime(start_date), pd.to_datetime(end_date)))
d = kpis(d)
stats = go.Figure()
fig ='number', value=stats['Total amount'], title={'text':'Total amount'}))
fig.add_trace(go.Indicator(mode='number', value=stats['Daily average'], title={'text':'Daily average'}, domain={'x':[0.35,0.65], 'y':[0,1]}))
fig.add_trace(go.Indicator(mode='number', value=stats['Unique days'], title={'text':'Unique days'}, domain={'x':[0.7,1], 'y':[0,1]}))
fig.add_trace(go.Indicator(mode={'rows':1,'columns':3}, height=200, margin=dict(l=10,r=10,t=10,b=10))
fig.update_layout(grid
fig.show()
render_kpis(w_nutrient.value, w_meal.value, w_start.value, w_end.value)
def _on_change(change):
render_kpis(w_nutrient.value, w_meal.value, w_start.value, w_end.value)
for w in [w_nutrient, w_meal, w_start, w_end]:
='value') w.observe(_on_change, names
π Trend (Daily + Rolling 7)
Daily totals with a 7-day rolling mean. Facets by Nutrient when βAllβ is selected.
def trend_figure(nutrient, meal, start_date, end_date):
= slice_data(df, nutrient, meal, (pd.to_datetime(start_date), pd.to_datetime(end_date)))
d if d.empty:
return go.Figure().add_annotation(text='No data in selection', showarrow=False)
= d.groupby(['Date','Nutrient'], as_index=False)['Amount'].sum().rename(columns={'Amount':'Total'})
daily 'Roll7'] = daily.sort_values('Date').groupby('Nutrient')['Total'].transform(lambda s: s.rolling(7, min_periods=1).mean())
daily[if nutrient == 'All':
= px.line(daily, x='Date', y='Total', color='Nutrient', title='Daily totals by Nutrient')
fig for n in daily['Nutrient'].unique():
= daily[daily['Nutrient']==n]
dn =dn['Date'], y=dn['Roll7'], name=f'{n} (7d MA)', mode='lines', line=dict(dash='dash')))
fig.add_trace(go.Scatter(xelse:
= px.line(daily, x='Date', y='Total', title=f'Daily totals: {nutrient}')
fig =daily['Date'], y=daily['Roll7'], name='7d MA', mode='lines', line=dict(dash='dash')))
fig.add_trace(go.Scatter(x=420, margin=dict(l=10,r=10,t=40,b=10))
fig.update_layout(heightreturn fig
= trend_figure(w_nutrient.value, w_meal.value, w_start.value, w_end.value)
fig_trend
fig_trend.show()
def _on_change_trend(change):
trend_figure(w_nutrient.value, w_meal.value, w_start.value, w_end.value).show()
for w in [w_nutrient, w_meal, w_start, w_end]:
='value') w.observe(_on_change_trend, names
π± Stacked Bars (Meal Composition)
Composition by Meal within the selected period (sum of Amount).
def stacked_meal(nutrient, meal, start_date, end_date):
= slice_data(df, nutrient, meal, (pd.to_datetime(start_date), pd.to_datetime(end_date)))
d if d.empty:
return go.Figure().add_annotation(text='No data in selection', showarrow=False)
= d.groupby(['Meal','Nutrient'], as_index=False)['Amount'].sum()
comp = px.bar(comp, x='Meal', y='Amount', color='Nutrient', title='Meal composition (sum of Amount)', barmode='stack')
fig =380, margin=dict(l=10,r=10,t=40,b=10))
fig.update_layout(heightreturn fig
= stacked_meal(w_nutrient.value, w_meal.value, w_start.value, w_end.value)
fig_bars
fig_bars.show()
def _on_change_bars(change):
stacked_meal(w_nutrient.value, w_meal.value, w_start.value, w_end.value).show()
for w in [w_nutrient, w_meal, w_start, w_end]:
='value') w.observe(_on_change_bars, names
π³ Treemap (Who eats what?)
Treemap by ID β Meal β Nutrient reveals heavy hitters and diet mix quickly.
def treemap_fig(nutrient, meal, start_date, end_date):
= slice_data(df, nutrient, meal, (pd.to_datetime(start_date), pd.to_datetime(end_date)))
d if d.empty:
return go.Figure().add_annotation(text='No data in selection', showarrow=False)
= d.groupby(['ID','Meal','Nutrient'], as_index=False)['Amount'].sum()
agg = px.treemap(agg, path=['ID','Meal','Nutrient'], values='Amount', title='Treemap: totals by ID β Meal β Nutrient')
fig =500, margin=dict(l=10,r=10,t=40,b=10))
fig.update_layout(heightreturn fig
= treemap_fig(w_nutrient.value, w_meal.value, w_start.value, w_end.value)
fig_tree
fig_tree.show()
def _on_change_tree(change):
treemap_fig(w_nutrient.value, w_meal.value, w_start.value, w_end.value).show()
for w in [w_nutrient, w_meal, w_start, w_end]:
='value') w.observe(_on_change_tree, names
πΎ Export Single-File HTML Dashboard
Write a self-contained dashboard.html
(no server needed) with KPIs + charts for your current selection.
def build_dashboard_html(nutrient, meal, start_date, end_date, out='dashboard.html'):
# Build figures with current selection
= go.Figure()
kpi_fig = slice_data(df, nutrient, meal, (pd.to_datetime(start_date), pd.to_datetime(end_date)))
d = kpis(d)
s ='number', value=s['Total amount'], title={'text':'Total amount'}))
kpi_fig.add_trace(go.Indicator(mode='number', value=s['Daily average'], title={'text':'Daily average'}, domain={'x':[0.35,0.65], 'y':[0,1]}))
kpi_fig.add_trace(go.Indicator(mode='number', value=s['Unique days'], title={'text':'Unique days'}, domain={'x':[0.7,1], 'y':[0,1]}))
kpi_fig.add_trace(go.Indicator(mode={'rows':1,'columns':3}, height=200, margin=dict(l=10,r=10,t=10,b=10))
kpi_fig.update_layout(grid
= [
figs 'KPIs', kpi_fig),
('Trend', trend_figure(nutrient, meal, start_date, end_date)),
('Meal Composition', stacked_meal(nutrient, meal, start_date, end_date)),
('Treemap', treemap_fig(nutrient, meal, start_date, end_date))
(
]= []
html_blocks for title, fig in figs:
f"<h2 style='font-family:sans-serif'>{title}</h2>" + fig.to_html(full_html=False, include_plotlyjs='cdn'))
html_blocks.append(= f"""
template <html>
<head>
<meta charset='utf-8'>
<title>Nutrition Dashboard</title>
<style>body{{max-width:1200px;margin:20px auto;font-family:sans-serif;}}</style>
</head>
<body>
<h1 style='font-family:sans-serif'>Nutrition Dashboard</h1>
<p><b>Filters:</b> Nutrient={nutrient} | Meal={meal} | Range={start_date} β {end_date}</p>
{''.join(html_blocks)}
</body>
</html>
"""
with open(out, 'w', encoding='utf-8') as f:
f.write(template)print('Wrote:', Path(out).resolve())
='dashboard.html') build_dashboard_html(w_nutrient.value, w_meal.value, w_start.value, w_end.value, out
β Optional: Quarto Template (Static/Parametric)
If you use Quarto, this writes a minimal .qmd
you can render outside Colab to a polished dashboard.
What you get
A starter Quarto page with Plotly support, filter parameters, and Python code chunks.= '''
qmd ---
title: "Nutrition Dashboard"
format:
html:
theme: cosmo
toc: false
filters:
- lightbox
execute:
echo: false
params:
nutrient: All
meal: All
start: 2024-01-01
end: 2024-12-31
---
```{python}
import pandas as pd, plotly.express as px
df = pd.read_csv('data/large_food_log.csv')
df['Date'] = pd.to_datetime(df['Date'], errors='coerce')
df = df.dropna(subset=['Amount','Date'])
nutrient = params['nutrient']
meal = params['meal']
start = pd.to_datetime(params['start'])
end = pd.to_datetime(params['end'])
d = df.copy()
if nutrient!='All': d = d[d['Nutrient']==nutrient]
if meal!='All': d = d[d['Meal']==meal]
d = d[(d['Date']>=start)&(d['Date']<=end)]
daily = d.groupby(['Date','Nutrient'], as_index=False)['Amount'].sum()
fig = px.line(daily, x='Date', y='Amount', color='Nutrient', title='Daily totals')
fig
```
'''
with open('nutrition_dashboard.qmd','w', encoding='utf-8') as f:
f.write(qmd)print('Wrote Quarto file:', Path('nutrition_dashboard.qmd').resolve())
π§© Exercises
Facet trends β Change the trend chart to facet by Meal when Nutrient is βAllβ.
Percent share β In the stacked bar, show percent composition per Meal (hint: compute totals per Meal, then divide and use
text_auto='0.0%'
).Anomaly hint β Add a line for 30-day rolling mean and scatter markers when daily total is 50% above the rolling mean.
Save & ship β Export different filter combinations as
dashboard_*.html
and share with your team.
β Conclusion
You built an interactive dashboard with Plotly + ipywidgets, added KPIs and multiple linked views, and exported a portable HTML. Youβre ready to package insights for nutrition stakeholders.
More
- Plotly Express: https://plotly.com/python/
- ipywidgets: https://ipywidgets.readthedocs.io/
- Quarto dashboards: https://quarto.org/docs/interactive/