Shiny
Shiny Dashboards Guide
Web presenters can create interactive dashboards using Python Shiny for rich server-side interactivity. Shiny dashboards provide immediate reactivity, complex data processing capabilities, and seamless integration with the analyzer pipeline. Which in turn allows developers and data scientists the ability to quickly prototype new analyses.
Overview
Shiny dashboards are server-rendered applications that provide:
- Real-time interactivity: Components update automatically when inputs change
- Server-side processing: Complex calculations run on the server with full Python ecosystem access
- Widgets: Built-in components for inputs, outputs, and visualizations
- Session management: Automatic handling of user sessions and state
Basic Structure
Every Shiny web presenter follows this pattern:
from shiny import reactive, render, ui
from shinywidgets import output_widget, render_widget
from analyzer_interface.context import WebPresenterContext, FactoryOutputContext, ShinyContext
import polars as pl
import plotly.express as px
def factory(context: WebPresenterContext) -> FactoryOutputContext:
# Load analyzer data
df = pl.read_parquet(context.base.table("your_output").parquet_path)
# Define UI layout
dashboard_ui = ui.card(
ui.card_header("Your Dashboard Title"),
ui.row(
ui.column(4,
# Input controls
ui.input_selectize("category", "Select Category",
choices=df["category"].unique().to_list()),
ui.input_slider("threshold", "Threshold", 0, 100, 50)
),
ui.column(8,
# Output displays
output_widget("main_plot", height="400px"),
ui.output_text("summary_stats")
)
)
)
def server(input, output, session):
@reactive.Calc
def filtered_data():
# Reactive data filtering
return df.filter(
(pl.col("category") == input.category()) &
(pl.col("value") >= input.threshold())
)
@render_widget
def main_plot():
# Create interactive plot
plot_df = filtered_data().to_pandas()
fig = px.scatter(plot_df, x="x", y="y", color="category")
return fig
@render.text
def summary_stats():
data = filtered_data()
return f"Showing {len(data)} items, avg value: {data['value'].mean():.2f}"
return FactoryOutputContext(
shiny=ShinyContext(
server_handler=server,
panel=nav_panel("Dashboard", dashboard_ui)
)
)
User Interface Components
Layout Components
Organize your dashboard with these layout elements:
# Cards for grouped content
ui.card(
ui.card_header("Section Title"),
ui.card_body("Content goes here")
)
# Grid layouts
ui.row(
ui.column(6, "Left column"),
ui.column(6, "Right column")
)
# Navigation
ui.navset_tab(
ui.nav_panel("Tab 1", "Content 1"),
ui.nav_panel("Tab 2", "Content 2")
)
# Sidebars
ui.sidebar(
"Sidebar content",
open="open" # or "closed"
)
Input Controls
Collect user input with various widgets:
# Text inputs
ui.input_text("text_id", "Label", value="default")
ui.input_text_area("textarea_id", "Description", rows=3)
# Numeric inputs
ui.input_numeric("number_id", "Number", value=10, min=0, max=100)
ui.input_slider("slider_id", "Range", 0, 100, value=[20, 80])
# Selection inputs
ui.input_select("select_id", "Choose one", choices=["A", "B", "C"])
ui.input_selectize("selectize_id", "Type to search",
choices=data["column"].unique().to_list(),
multiple=True)
# Boolean inputs
ui.input_checkbox("check_id", "Enable feature", value=True)
ui.input_switch("switch_id", "Toggle mode")
# File uploads
ui.input_file("file_id", "Upload CSV", accept=".csv")
# Date/time inputs
ui.input_date("date_id", "Select date")
ui.input_date_range("daterange_id", "Date range")
Output Components
Display results with these output components:
# Text outputs
ui.output_text("text_id") # Plain text
ui.output_text_verbatim("code_id") # Monospace text
ui.output_ui("dynamic_ui") # Dynamic UI elements
# Tables
ui.output_table("table_id") # Basic table
ui.output_data_frame("df_id") # Interactive data frame
# Plots
output_widget("plot_id") # For plotly/bokeh widgets
ui.output_plot("matplotlib_id") # For matplotlib plots
# Downloads
ui.download_button("download_id", "Download Data")
Reactive Programming
Shiny's reactive system automatically updates outputs when inputs change:
Reactive Calculations
Use @reactive.Calc
for expensive computations that multiple outputs depend on:
@reactive.Calc
def processed_data():
# This only runs when dependencies change
raw_data = load_data()
return raw_data.filter(pl.col("active") == input.show_active())
@render_widget
def plot1():
data = processed_data() # Uses cached result
return create_plot(data)
@render.text
def summary():
data = processed_data() # Uses same cached result
return f"Records: {len(data)}"
Reactive Effects
Use @reactive.Effect
for side effects like updating other inputs:
@reactive.Effect
def update_choices():
# Update selectize choices when category changes
category = input.category()
new_choices = df.filter(pl.col("category") == category)["subcategory"].unique()
ui.update_selectize("subcategory", choices=new_choices.to_list())
Event Handling
Respond to button clicks and other events:
@reactive.Effect
@reactive.event(input.reset_button)
def reset_filters():
ui.update_slider("threshold", value=50)
ui.update_select("category", selected="All")
Data Visualization
Plotly Integration
Create interactive plots with plotly:
from shinywidgets import output_widget, render_widget
import plotly.express as px
import plotly.graph_objects as go
@render_widget
def scatter_plot():
df_plot = filtered_data().to_pandas()
fig = px.scatter(
df_plot,
x="x_value",
y="y_value",
color="category",
size="size_value",
hover_data=["additional_info"],
title="Interactive Scatter Plot"
)
# Customize layout
fig.update_layout(
height=500,
showlegend=True,
hovermode="closest"
)
return fig
@render_widget
def time_series():
df_ts = time_series_data().to_pandas()
fig = go.Figure()
for category in df_ts["category"].unique():
category_data = df_ts[df_ts["category"] == category]
fig.add_trace(go.Scatter(
x=category_data["date"],
y=category_data["value"],
name=category,
mode="lines+markers"
))
fig.update_layout(
title="Time Series Analysis",
xaxis_title="Date",
yaxis_title="Value"
)
return fig
Custom Plots
Create custom visualizations with matplotlib or other libraries:
from shiny import render
import matplotlib.pyplot as plt
import seaborn as sns
@render.plot
def correlation_heatmap():
df_corr = correlation_data().to_pandas()
plt.figure(figsize=(10, 8))
sns.heatmap(
df_corr.corr(),
annot=True,
cmap="coolwarm",
center=0,
square=True
)
plt.title("Correlation Matrix")
plt.tight_layout()
return plt.gcf()
Data Tables
Display and interact with tabular data:
Basic Tables
@render.table
def simple_table():
return filtered_data().to_pandas()
Interactive Data Frames
from shiny.render import DataGrid, DataTable
@render.data_frame
def interactive_grid():
df_display = filtered_data().to_pandas()
return DataGrid(
df_display,
selection_mode="rows", # or "none", "row", "rows", "col", "cols"
filters=True,
width="100%",
height="400px"
)
# Access selected rows
@reactive.Effect
def handle_selection():
selected = interactive_grid.data_view(selected=True)
if len(selected) > 0:
# Process selected data
pass
Custom Table Styling
@render.table
def styled_table():
df = summary_stats().to_pandas()
# Format numeric columns
df["percentage"] = df["percentage"].map("{:.1%}".format)
df["amount"] = df["amount"].map("${:,.0f}".format)
return df
Advanced Features
Dynamic UI
Create UI elements that change based on user input:
@render.ui
def dynamic_controls():
analysis_type = input.analysis_type()
if analysis_type == "correlation":
return ui.div(
ui.input_selectize("x_var", "X Variable", choices=numeric_columns),
ui.input_selectize("y_var", "Y Variable", choices=numeric_columns)
)
if analysis_type == "distribution":
return ui.div(
ui.input_select("dist_var", "Variable", choices=all_columns),
ui.input_numeric("bins", "Number of bins", value=30)
)
eturn ui.div("Select an analysis type")
Progress Indicators
Show progress for long-running operations:
from shiny import ui
@reactive.Effect
@reactive.event(input.run_analysis)
def run_long_analysis():
with ui.Progress(min=0, max=100) as progress:
progress.set(message="Loading data", value=0)
data = load_large_dataset()
progress.set(message="Processing", value=50)
results = process_data(data)
progress.set(message="Finalizing", value=90)
save_results(results)
progress.set(value=100)
ui.notification_show("Analysis complete!", type="success")
Integration with Analyzers
Accessing Analyzer Data
def factory(context: WebPresenterContext) -> FactoryOutputContext:
# Access primary analyzer outputs
main_data = pl.read_parquet(
context.base.table("main_analysis").parquet_path
)
# Access secondary analyzer outputs
summary_data = pl.read_parquet(
context.dependency(summary_analyzer).table("summary").parquet_path
)
# Access parameters used in analysis
threshold = context.base_params.get("threshold", 0.5)
# Build dashboard with this data
# ...
Parameter Integration
Use analyzer parameters in your dashboard:
def server(input, output, session):
# Get analyzer parameters
analyzer_threshold = context.base_params.get("threshold", 0.5)
@render.text
def analysis_info():
return f"Analysis run with threshold: {analyzer_threshold}"
@render_widget
def threshold_comparison():
# Compare user input with analyzer parameter
user_threshold = input.user_threshold()
df_comparison = main_data.with_columns([
(pl.col("value") > analyzer_threshold).alias("analyzer_flag"),
(pl.col("value") > user_threshold).alias("user_flag")
])
return create_comparison_plot(df_comparison)
Performance Optimization
Efficient Data Processing
@reactive.Calc
def base_data():
# Load once and cache
return pl.read_parquet(data_path)
@reactive.Calc
def filtered_data():
# Efficient filtering with Polars
filters = []
if input.category() != "All":
filters.append(pl.col("category") == input.category())
if input.date_range() is not None:
start, end = input.date_range()
filters.append(pl.col("date").is_between(start, end))
if filters:
return base_data().filter(pl.all_horizontal(filters))
else:
return base_data()
Lazy Evaluation
@reactive.Calc
def expensive_calculation():
# Only runs when dependencies change
data = filtered_data()
# Use lazy evaluation
result = (
data
.group_by("category")
.agg([
pl.col("value").mean().alias("avg_value"),
pl.col("value").std().alias("std_value"),
pl.col("value").count().alias("count")
])
.sort("avg_value", descending=True)
)
return result
Testing Shiny Dashboards
Unit Testing Components
import pytest
from shiny.testing import ShinyAppProc
from your_presenter import factory
def test_dashboard_loads():
"""Test that dashboard loads without errors"""
app = factory(mock_context)
# Test UI renders
assert app.shiny.panel is not None
# Test server function exists
assert callable(app.shiny.server_handler)
def test_data_filtering():
"""Test reactive data filtering"""
with ShinyAppProc(factory(mock_context)) as proc:
# Set input values
proc.set_inputs(category="TypeA", threshold=50)
# Check outputs update correctly
output = proc.get_output("summary_stats")
assert "TypeA" in output
Integration Testing
def test_with_real_data():
"""Test dashboard with actual analyzer output"""
# Run analyzer to generate test data
context = create_test_context(test_data_path)
# Test dashboard with real data
app = factory(context)
# Verify data loads correctly
assert app.shiny.panel is not None
Deployment Considerations
Resource Management
- Use
@reactive.Calc
for expensive operations to enable caching - Implement pagination for large datasets
- Consider data sampling for very large visualizations
- Use lazy loading for secondary data
Error Handling
@render_widget
def safe_plot():
try:
data = filtered_data()
if len(data) == 0:
return empty_plot_message()
return create_plot(data)
except Exception as e:
ui.notification_show(f"Plot error: {str(e)}", type="error")
return error_plot()
Session Management
def server(input, output, session):
# Clean up resources when session ends
@reactive.Effect
def cleanup():
session.on_ended(lambda: cleanup_user_data())
Example: Complete Dashboard
Here's a complete example of a Shiny dashboard for analyzing message sentiment:
from shiny import reactive, render, ui
from shinywidgets import output_widget, render_widget
import plotly.express as px
import polars as pl
def factory(context: WebPresenterContext) -> FactoryOutputContext:
# Load data
df_sentiment = pl.read_parquet(
context.base.table("sentiment_analysis").parquet_path
)
# Get unique values for inputs
date_range = (df_sentiment["date"].min(), df_sentiment["date"].max())
categories = ["All"] + df_sentiment["category"].unique().to_list()
# UI Layout
dashboard = ui.page_sidebar(
ui.sidebar(
ui.h3("Analysis Controls"),
ui.input_date_range(
"date_filter",
"Date Range",
start=date_range[0],
end=date_range[1]
),
ui.input_selectize(
"category_filter",
"Categories",
choices=categories,
selected="All",
multiple=True
),
ui.input_slider(
"sentiment_threshold",
"Sentiment Threshold",
-1, 1, 0, step=0.1
),
ui.hr(),
ui.input_action_button("reset", "Reset Filters"),
ui.download_button("download", "Download Data")
),
ui.div(
ui.h2("Sentiment Analysis Dashboard"),
ui.row(
ui.column(6, ui.value_box(
title="Total Messages",
value=ui.output_text("total_count"),
theme="primary"
)),
ui.column(6, ui.value_box(
title="Avg Sentiment",
value=ui.output_text("avg_sentiment"),
theme="success"
))
),
ui.navset_tab(
ui.nav_panel(
"Time Series",
output_widget("timeseries_plot", height="500px")
),
ui.nav_panel(
"Distribution",
output_widget("distribution_plot", height="500px")
),
ui.nav_panel(
"Data Table",
ui.output_data_frame("data_table")
)
)
)
)
def server(input, output, session):
@reactive.Calc
def filtered_data():
data = df_sentiment
# Date filtering
if input.date_filter() is not None:
start, end = input.date_filter()
data = data.filter(pl.col("date").is_between(start, end))
# Category filtering
if "All" not in input.category_filter():
data = data.filter(pl.col("category").is_in(input.category_filter()))
# Sentiment filtering
data = data.filter(pl.col("sentiment") >= input.sentiment_threshold())
return data
@render.text
def total_count():
return f"{len(filtered_data()):,}"
@render.text
def avg_sentiment():
avg = filtered_data()["sentiment"].mean()
return f"{avg:.3f}"
@render_widget
def timeseries_plot():
df_plot = (
filtered_data()
.group_by("date")
.agg(pl.col("sentiment").mean().alias("avg_sentiment"))
.sort("date")
.to_pandas()
)
fig = px.line(
df_plot,
x="date",
y="avg_sentiment",
title="Sentiment Over Time"
)
fig.add_hline(y=0, line_dash="dash", line_color="gray")
return fig
@render_widget
def distribution_plot():
df_plot = filtered_data().to_pandas()
fig = px.histogram(
df_plot,
x="sentiment",
color="category",
title="Sentiment Distribution",
nbins=50
)
return fig
@render.data_frame
def data_table():
return filtered_data().to_pandas()
@reactive.Effect
@reactive.event(input.reset)
def reset_filters():
ui.update_date_range("date_filter", start=date_range[0], end=date_range[1])
ui.update_selectize("category_filter", selected="All")
ui.update_slider("sentiment_threshold", value=0)
@render.download(filename="sentiment_data.csv")
def download():
return filtered_data().write_csv()
return FactoryOutputContext(
shiny=ShinyContext(
server_handler=server,
panel=nav_panel("Sentiment Analysis", dashboard)
)
)
This comprehensive guide covers all aspects of building Shiny dashboards for your analyzer platform. The reactive programming model, rich widget ecosystem, and seamless Python integration make Shiny an excellent choice for creating sophisticated data analysis interfaces.
Next Steps
Once you finish reading section be a good idea to review the section that discuss implementing React dashboards. Might also be a good idea to review the sections for each domain.