Chapter 15: Key Takeaways - Interactive Dashboards
Quick Reference Card
Framework Selection Guide
| Framework |
Best For |
Learning Curve |
Deployment |
| Plotly |
Interactive charts in notebooks/web |
Low |
HTML export |
| Dash |
Full web apps with callbacks |
Medium |
Heroku, AWS, Docker |
| Streamlit |
Rapid prototyping, data apps |
Low |
Streamlit Cloud |
| Bokeh |
Custom interactivity, large data |
High |
Bokeh Server |
Interaction Types
| Type |
Description |
Example |
| Filter |
Narrow data to subsets |
Conference dropdown |
| Select |
Highlight specific elements |
Click team point |
| Drill |
Navigate detail levels |
Season → Game → Play |
| Link |
Connect views together |
Cross-filtering |
| Hover |
Show tooltips |
Play description |
| Zoom |
Focus on region |
Time range selection |
Dash Callback Patterns
Basic Callback
@callback(
Output('chart', 'figure'),
Input('dropdown', 'value')
)
def update_chart(selected_value):
filtered = df[df['column'] == selected_value]
return px.scatter(filtered, x='x', y='y')
Multiple Outputs
@callback(
Output('chart1', 'figure'),
Output('chart2', 'figure'),
Output('summary', 'children'),
Input('filter', 'value')
)
def update_all(filter_value):
filtered = apply_filter(df, filter_value)
return create_chart1(filtered), create_chart2(filtered), create_summary(filtered)
Cross-Filtering
@callback(
Output('chart', 'figure'),
Input('chart', 'selectedData'),
Input('dropdown', 'value')
)
def cross_filter(selection, dropdown_value):
filtered = df.copy()
if selection:
selected_ids = [p['customdata'] for p in selection['points']]
filtered = filtered[filtered['id'].isin(selected_ids)]
return create_figure(filtered)
Using State
@callback(
Output('result', 'children'),
Input('submit-button', 'n_clicks'),
State('input-field', 'value'),
prevent_initial_call=True
)
def submit_form(n_clicks, input_value):
return f"Submitted: {input_value}"
Streamlit Patterns
Basic Layout
st.title("Dashboard Title")
# Sidebar
with st.sidebar:
filter_value = st.selectbox("Filter", options)
# Columns
col1, col2 = st.columns(2)
with col1:
st.metric("Metric", value)
with col2:
st.plotly_chart(fig)
Caching
@st.cache_data(ttl=3600) # Cache for 1 hour
def load_data():
return pd.read_csv("large_file.csv")
@st.cache_resource # For models, connections
def load_model():
return joblib.load("model.pkl")
Session State
if 'counter' not in st.session_state:
st.session_state.counter = 0
if st.button("Increment"):
st.session_state.counter += 1
st.write(f"Count: {st.session_state.counter}")
Plotly Templates
Interactive Scatter
fig = px.scatter(df, x='off_epa', y='def_epa',
size='wins', color='conference',
hover_name='team',
title='Team Efficiency')
fig.update_layout(template='plotly_white',
hovermode='closest')
Hover Template
fig.update_traces(
hovertemplate='<b>%{hovertext}</b><br>' +
'Off EPA: %{x:.3f}<br>' +
'Def EPA: %{y:.3f}<br>' +
'<extra></extra>'
)
Annotations
fig.add_annotation(
x=0.25, y=-0.20,
text="Key moment",
showarrow=True,
arrowhead=2
)
Caching Strategies
# Streamlit
@st.cache_data(ttl=3600)
def load_data(): ...
# Dash
from flask_caching import Cache
cache = Cache(app.server, config={'CACHE_TYPE': 'simple'})
@cache.memoize(timeout=3600)
def expensive_computation(): ...
Lazy Loading
# Load on demand, not on page load
@callback(
Output('data-table', 'data'),
Input('load-button', 'n_clicks'),
prevent_initial_call=True
)
def load_on_click(n):
return load_large_dataset().to_dict('records')
Data Aggregation
def reduce_for_display(df, max_points=1000):
if len(df) > max_points:
return df.sample(max_points)
return df
Design Principles Checklist
- [ ] Progressive disclosure: Start simple, reveal complexity on demand
- [ ] Responsive feedback: Show loading states, confirm actions
- [ ] Consistent interactions: Same gesture = same result everywhere
- [ ] Information hierarchy: Key metrics visible first
- [ ] Empty states: Meaningful message when no data matches
- [ ] Error handling: Graceful failure with helpful messages
Common Callbacks
Filter Update
@callback(Output('chart', 'figure'), Input('filter', 'value'))
def update(val):
return create_fig(df[df['col'] == val] if val else df)
Click Selection
@callback(Output('details', 'children'), Input('chart', 'clickData'))
def show_details(click):
if not click:
return "Click a point"
return click['points'][0]['hovertext']
Range Selection
@callback(Output('summary', 'children'), Input('chart', 'relayoutData'))
def on_zoom(relayout):
if relayout and 'xaxis.range[0]' in relayout:
start = relayout['xaxis.range[0]']
end = relayout['xaxis.range[1]']
return f"Selected: {start} to {end}"
Deployment Commands
Streamlit
# Local
streamlit run app.py
# Cloud
# Push to GitHub, connect Streamlit Cloud
Dash (Gunicorn)
gunicorn app:server -b 0.0.0.0:8050
Docker
FROM python:3.10
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
EXPOSE 8050
CMD ["gunicorn", "app:server", "-b", "0.0.0.0:8050"]
Key Terminology
| Term |
Definition |
| Callback |
Function triggered by user interaction |
| Cross-filtering |
Selection in one chart filters all charts |
| Drill-down |
Navigate from summary to detail |
| Lazy loading |
Load data only when requested |
| Progressive disclosure |
Reveal complexity gradually |
| Session state |
Data persisted across user interactions |
| Throttling |
Limit update frequency for performance |
Quick Decision Tree
What do you need?
├── Quick exploration in notebook → Plotly
│
├── Simple data app → Streamlit
│
├── Full web application
│ ├── Need callbacks/state → Dash
│ └── Need custom interactivity → Bokeh
│
└── Embeddable chart → Plotly (export HTML)
Common Mistakes to Avoid
- Loading all data on startup → Use lazy loading
- Rebuilding entire figure on small changes → Update only what changed
- No loading indicators → Users think app is broken
- Filters at bottom of page → Put filters where users look first
- No empty state handling → Show message when filters return no data
- Ignoring mobile users → Test on tablets and phones