Case Study 2: Creating Visualizations for Multiple Audiences
Overview
Context: Conference championship game analytics package Challenge: Same underlying data needed for four distinct audiences Deliverables: Broadcast graphics, social media content, coaching materials, and executive presentation Learning Focus: Audience-adaptive visualization design
The Scenario
State University is preparing for the conference championship game against Rival University. The analytics team has been asked to create visualizations for four different audiences using the same underlying performance data:
- TV Broadcast: Graphics for the game broadcast
- Social Media: Fan engagement content
- Coaching Staff: Game preparation materials
- Athletic Director: Board presentation on program analytics
The core dataset includes season-long performance metrics for both teams:
matchup_data = {
'State': {
'record': '11-1',
'epa_offense': 0.215,
'epa_defense': 0.142,
'success_rate_off': 0.472,
'success_rate_def': 0.388,
'explosive_rate': 0.134,
'turnover_margin': +8,
'red_zone_td_pct': 0.724,
'third_down_pct': 0.442,
'points_per_game': 38.2,
'points_allowed': 18.5,
'key_player': 'QB Johnson - 3,412 yds, 32 TD'
},
'Rival': {
'record': '10-2',
'epa_offense': 0.188,
'epa_defense': 0.165,
'success_rate_off': 0.445,
'success_rate_def': 0.402,
'explosive_rate': 0.098,
'turnover_margin': +5,
'red_zone_td_pct': 0.682,
'third_down_pct': 0.398,
'points_per_game': 32.8,
'points_allowed': 16.2,
'key_player': 'RB Smith - 1,687 yds, 18 TD'
}
}
Audience 1: TV Broadcast Graphics
Constraints
- View time: 5-8 seconds per graphic
- Distance: Viewed on various screen sizes from 10+ feet
- Context: Viewers may be casually watching, not studying
- Format: 1920Γ1080 HD, must work with lower-third banners
Design Approach
Pre-Game Comparison Graphic
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
def create_broadcast_comparison():
"""Create broadcast-ready team comparison graphic."""
fig, ax = plt.subplots(figsize=(16, 9))
fig.patch.set_facecolor('#1a1a2e')
ax.set_facecolor('#1a1a2e')
# Team colors
state_color = '#CC0033'
rival_color = '#003366'
# Title area
ax.text(0.5, 0.92, 'CHAMPIONSHIP MATCHUP',
ha='center', va='center', fontsize=36,
color='white', fontweight='bold',
transform=ax.transAxes)
# Team names and records
ax.text(0.25, 0.78, 'STATE', ha='center', fontsize=32,
color=state_color, fontweight='bold', transform=ax.transAxes)
ax.text(0.25, 0.70, '11-1', ha='center', fontsize=24,
color='white', transform=ax.transAxes)
ax.text(0.75, 0.78, 'RIVAL', ha='center', fontsize=32,
color=rival_color, fontweight='bold', transform=ax.transAxes)
ax.text(0.75, 0.70, '10-2', ha='center', fontsize=24,
color='white', transform=ax.transAxes)
# VS badge
circle = plt.Circle((0.5, 0.74), 0.06, color='gold',
transform=ax.transAxes, zorder=10)
ax.add_patch(circle)
ax.text(0.5, 0.74, 'VS', ha='center', va='center',
fontsize=20, color='#1a1a2e', fontweight='bold',
transform=ax.transAxes, zorder=11)
# Key stats comparison (only 3 stats for broadcast)
stats = [
('POINTS/GAME', 38.2, 32.8),
('TURNOVER MARGIN', '+8', '+5'),
('RED ZONE TD%', '72%', '68%')
]
y_positions = [0.50, 0.35, 0.20]
for (label, state_val, rival_val), y in zip(stats, y_positions):
# Label
ax.text(0.5, y + 0.05, label, ha='center', fontsize=16,
color='#888888', transform=ax.transAxes)
# Values
ax.text(0.25, y - 0.02, str(state_val), ha='center', fontsize=28,
color='white', fontweight='bold', transform=ax.transAxes)
ax.text(0.75, y - 0.02, str(rival_val), ha='center', fontsize=28,
color='white', fontweight='bold', transform=ax.transAxes)
# Highlight winner
if isinstance(state_val, (int, float)) and state_val > rival_val:
ax.text(0.25, y - 0.02, str(state_val), ha='center', fontsize=28,
color=state_color, fontweight='bold', transform=ax.transAxes)
elif isinstance(rival_val, (int, float)) and rival_val > state_val:
ax.text(0.75, y - 0.02, str(rival_val), ha='center', fontsize=28,
color=rival_color, fontweight='bold', transform=ax.transAxes)
# Remove axes
ax.axis('off')
return fig
Key Design Decisions: 1. Limited metrics: Only 3 stats vs. original 12βviewers can't process more in 5 seconds 2. Large text: Minimum 16pt for labels, 28pt+ for key values 3. High contrast: White text on dark background 4. Visual hierarchy: Team names largest, values prominent, labels subdued 5. Winner highlighting: Color indicates which team leads each stat
In-Game Win Probability Graphic
def create_wp_broadcast(current_wp, time_remaining, score):
"""Create in-game win probability display for broadcast."""
fig, ax = plt.subplots(figsize=(8, 3))
fig.patch.set_facecolor('#1a1a2e')
ax.set_facecolor('#1a1a2e')
# Simple gauge showing current WP
wp = current_wp # e.g., 0.73
# Background bar
ax.barh(0, 1.0, height=0.4, color='#333333')
# State's portion
ax.barh(0, wp, height=0.4, color='#CC0033')
# Percentage labels
ax.text(wp/2, 0, f'{int(wp*100)}%', ha='center', va='center',
fontsize=24, color='white', fontweight='bold')
ax.text((1+wp)/2, 0, f'{int((1-wp)*100)}%', ha='center', va='center',
fontsize=24, color='white', fontweight='bold')
# Team labels
ax.text(0.02, 0.55, 'STATE', ha='left', fontsize=14,
color='#CC0033', fontweight='bold', transform=ax.transAxes)
ax.text(0.98, 0.55, 'RIVAL', ha='right', fontsize=14,
color='#003366', fontweight='bold', transform=ax.transAxes)
# Title
ax.text(0.5, 0.85, 'WIN PROBABILITY', ha='center', fontsize=12,
color='#888888', transform=ax.transAxes)
ax.set_xlim(-0.02, 1.02)
ax.set_ylim(-0.5, 0.8)
ax.axis('off')
return fig
Audience 2: Social Media Content
Constraints
- View time: 2-3 seconds scroll time
- Format: Square (1080Γ1080) for Instagram, 16:9 for Twitter
- Context: Competing with hundreds of other posts
- Goal: Engagement (likes, shares, comments)
Design Approach
Comparison Infographic (Instagram)
def create_social_comparison():
"""Create social media friendly comparison graphic."""
fig, ax = plt.subplots(figsize=(10.8, 10.8)) # Square for Instagram
fig.patch.set_facecolor('#ffffff')
ax.set_facecolor('#ffffff')
# Bold header
ax.text(0.5, 0.95, 'π CHAMPIONSHIP', ha='center', fontsize=28,
fontweight='bold', transform=ax.transAxes)
ax.text(0.5, 0.89, 'BY THE NUMBERS', ha='center', fontsize=22,
color='#666666', transform=ax.transAxes)
# Team headers with emoji flags
ax.text(0.22, 0.80, 'π΄ STATE', ha='center', fontsize=20,
fontweight='bold', color='#CC0033', transform=ax.transAxes)
ax.text(0.78, 0.80, 'π΅ RIVAL', ha='center', fontsize=20,
fontweight='bold', color='#003366', transform=ax.transAxes)
# Comparison items with visual bars
comparisons = [
('Points/Game', 38.2, 32.8, 'π―'),
('Success Rate', 47.2, 44.5, 'β
'),
('Explosive Plays', 13.4, 9.8, 'π₯'),
('Turnovers', '+8', '+5', 'π'),
]
y_start = 0.68
y_step = 0.15
for i, (label, state_val, rival_val, emoji) in enumerate(comparisons):
y = y_start - i * y_step
# Emoji and label
ax.text(0.5, y + 0.04, f'{emoji} {label}', ha='center', fontsize=16,
color='#333333', transform=ax.transAxes)
# Values with size indicating winner
state_size = 24 if float(str(state_val).replace('+', '')) >= float(str(rival_val).replace('+', '')) else 18
rival_size = 24 if float(str(rival_val).replace('+', '')) >= float(str(state_val).replace('+', '')) else 18
ax.text(0.22, y - 0.03, str(state_val), ha='center',
fontsize=state_size, fontweight='bold',
color='#CC0033', transform=ax.transAxes)
ax.text(0.78, y - 0.03, str(rival_val), ha='center',
fontsize=rival_size, fontweight='bold',
color='#003366', transform=ax.transAxes)
# Call to action
ax.text(0.5, 0.08, 'Who ya got? π', ha='center', fontsize=18,
color='#666666', transform=ax.transAxes)
# Hashtag
ax.text(0.5, 0.03, '#ChampionshipGame #CollegeFootball',
ha='center', fontsize=12, color='#999999', transform=ax.transAxes)
ax.axis('off')
return fig
Key Design Decisions: 1. Emoji usage: Adds visual interest and communicates quickly 2. Call to action: "Who ya got?" encourages comments 3. Hashtags: Increases discoverability 4. Bold contrasts: Works even as a small thumbnail 5. Limited text: ~50 words maximum 6. Simple comparison: Visual weight shows winner
"Hot Take" Stat Graphic
def create_hot_take_graphic():
"""Create attention-grabbing single stat graphic."""
fig, ax = plt.subplots(figsize=(10.8, 10.8))
fig.patch.set_facecolor('#CC0033')
ax.set_facecolor('#CC0033')
# Giant stat
ax.text(0.5, 0.55, '+8', ha='center', va='center', fontsize=120,
fontweight='bold', color='white', transform=ax.transAxes)
# Context
ax.text(0.5, 0.30, 'TURNOVER MARGIN', ha='center', fontsize=28,
fontweight='bold', color='white', transform=ax.transAxes)
ax.text(0.5, 0.22, 'Best in the Conference', ha='center', fontsize=18,
color='#ffcccc', transform=ax.transAxes)
# Team branding
ax.text(0.5, 0.85, 'STATE FOOTBALL', ha='center', fontsize=24,
fontweight='bold', color='white', transform=ax.transAxes)
ax.axis('off')
return fig
Audience 3: Coaching Staff
Constraints
- View time: Extended study (minutes to hours)
- Format: Print-ready and tablet-optimized
- Context: Film room, practice planning, game prep
- Goal: Actionable insights for game planning
Design Approach
Situational Tendency Report
def create_coaching_report():
"""Create detailed coaching analysis visualization."""
fig = plt.figure(figsize=(11, 8.5)) # Letter size for printing
# Create grid layout
gs = fig.add_gridspec(3, 3, hspace=0.3, wspace=0.3,
left=0.08, right=0.95, top=0.90, bottom=0.08)
# Title
fig.suptitle('RIVAL UNIVERSITY - OFFENSIVE TENDENCIES',
fontsize=16, fontweight='bold', y=0.96)
fig.text(0.5, 0.92, 'Based on last 5 games | Prepared for Championship Week',
ha='center', fontsize=10, color='gray')
# 1. Down & Distance Heatmap (spans 2 columns)
ax1 = fig.add_subplot(gs[0, :2])
create_tendency_heatmap(ax1)
# 2. Formation Distribution
ax2 = fig.add_subplot(gs[0, 2])
create_formation_pie(ax2)
# 3. Red Zone Tendencies
ax3 = fig.add_subplot(gs[1, 0])
create_redzone_breakdown(ax3)
# 4. Third Down Analysis
ax4 = fig.add_subplot(gs[1, 1])
create_third_down_chart(ax4)
# 5. Key Player Focus
ax5 = fig.add_subplot(gs[1, 2])
create_player_focus(ax5)
# 6. Drive Chart (spans full width)
ax6 = fig.add_subplot(gs[2, :])
create_drive_summary(ax6)
return fig
def create_tendency_heatmap(ax):
"""Down Γ Distance play-calling tendencies."""
data = np.array([
[0.45, 0.38, 0.35, 0.30], # 1st down: run %
[0.42, 0.35, 0.28, 0.22], # 2nd down
[0.25, 0.18, 0.12, 0.08] # 3rd down
])
im = ax.imshow(data, cmap='RdYlGn_r', aspect='auto', vmin=0, vmax=0.5)
# Annotations
for i in range(3):
for j in range(4):
text = f'{data[i,j]:.0%}'
ax.text(j, i, text, ha='center', va='center', fontsize=10)
ax.set_xticks(range(4))
ax.set_xticklabels(['1-3', '4-6', '7-10', '11+'], fontsize=9)
ax.set_yticks(range(3))
ax.set_yticklabels(['1st', '2nd', '3rd'], fontsize=9)
ax.set_xlabel('Distance', fontsize=10)
ax.set_ylabel('Down', fontsize=10)
ax.set_title('Run % by Situation', fontsize=11, fontweight='bold')
# Colorbar
cbar = plt.colorbar(im, ax=ax, shrink=0.8)
cbar.ax.tick_params(labelsize=8)
def create_formation_pie(ax):
"""Formation usage breakdown."""
formations = ['11 Personnel', '12 Personnel', '21 Personnel', 'Other']
sizes = [45, 28, 18, 9]
colors = ['#2d4a6f', '#4a7ca8', '#7eb5d6', '#b8d4e8']
ax.pie(sizes, labels=formations, autopct='%1.0f%%', colors=colors,
textprops={'fontsize': 8})
ax.set_title('Personnel Groupings', fontsize=11, fontweight='bold')
def create_redzone_breakdown(ax):
"""Red zone play calling."""
plays = ['Run Inside', 'Run Outside', 'Pass Short', 'Pass Deep']
values = [35, 20, 30, 15]
bars = ax.barh(plays, values, color='#CC0033')
ax.set_xlim(0, 50)
ax.set_xlabel('% of Plays', fontsize=9)
ax.set_title('Red Zone Tendencies', fontsize=11, fontweight='bold')
for bar, val in zip(bars, values):
ax.text(val + 1, bar.get_y() + bar.get_height()/2,
f'{val}%', va='center', fontsize=9)
def create_third_down_chart(ax):
"""Third down conversion analysis."""
distances = ['Short (1-3)', 'Medium (4-6)', 'Long (7+)']
conv_rate = [72, 48, 28]
colors = ['green', 'orange', 'red']
bars = ax.bar(distances, conv_rate, color=colors, alpha=0.7)
ax.set_ylabel('Conv. Rate %', fontsize=9)
ax.set_title('3rd Down Conversion', fontsize=11, fontweight='bold')
ax.set_ylim(0, 100)
for bar, val in zip(bars, conv_rate):
ax.text(bar.get_x() + bar.get_width()/2, val + 2,
f'{val}%', ha='center', fontsize=9)
def create_player_focus(ax):
"""Key player analysis."""
ax.text(0.5, 0.85, 'RB SMITH', ha='center', fontsize=14,
fontweight='bold', transform=ax.transAxes)
ax.text(0.5, 0.70, '#22', ha='center', fontsize=12,
color='gray', transform=ax.transAxes)
stats = [
'Carries: 245 (20.4/game)',
'Yards: 1,687 (6.9 YPC)',
'TDs: 18',
'Broken Tackles: 47',
'Yards After Contact: 3.8'
]
for i, stat in enumerate(stats):
ax.text(0.5, 0.55 - i*0.12, stat, ha='center', fontsize=9,
transform=ax.transAxes)
ax.text(0.5, 0.08, 'β οΈ Key threat on outside zone',
ha='center', fontsize=9, color='red', transform=ax.transAxes)
ax.axis('off')
ax.set_title('Key Player', fontsize=11, fontweight='bold')
def create_drive_summary(ax):
"""Last 5 games drive outcomes."""
games = ['Game 8', 'Game 9', 'Game 10', 'Game 11', 'Game 12']
tds = [4, 5, 3, 6, 4]
fgs = [2, 1, 2, 1, 2]
punts = [3, 2, 4, 2, 3]
turnovers = [1, 1, 2, 0, 1]
x = np.arange(len(games))
width = 0.2
ax.bar(x - 1.5*width, tds, width, label='TD', color='green')
ax.bar(x - 0.5*width, fgs, width, label='FG', color='blue')
ax.bar(x + 0.5*width, punts, width, label='Punt', color='gray')
ax.bar(x + 1.5*width, turnovers, width, label='TO', color='red')
ax.set_xticks(x)
ax.set_xticklabels(games, fontsize=9)
ax.set_ylabel('Drives', fontsize=9)
ax.set_title('Drive Outcomes - Last 5 Games', fontsize=11, fontweight='bold')
ax.legend(loc='upper right', fontsize=8)
Key Design Decisions: 1. Dense information: Coaches need comprehensive data 2. Print-optimized: Clear at 300 DPI on letter paper 3. Structured layout: Information hierarchy through grid 4. Actionable callouts: Key warnings highlighted in red 5. Multiple chart types: Different insights need different views
Audience 4: Athletic Director Presentation
Constraints
- View time: 2-3 minutes per slide
- Format: PowerPoint-ready, projected in boardroom
- Context: Budget discussions, program evaluation
- Goal: Demonstrate analytics ROI and program success
Design Approach
Program Success Overview
def create_executive_slide():
"""Create executive presentation slide."""
fig, axes = plt.subplots(1, 3, figsize=(14, 5))
fig.patch.set_facecolor('white')
# Slide title (would be in PowerPoint, shown here for context)
fig.suptitle('Analytics-Driven Performance Improvement',
fontsize=18, fontweight='bold', y=1.02)
# Chart 1: Year-over-year EPA improvement
ax1 = axes[0]
years = ['2021', '2022', '2023', '2024']
epa = [0.08, 0.12, 0.18, 0.22]
bars = ax1.bar(years, epa, color=['#cccccc', '#999999', '#666666', '#CC0033'])
ax1.set_ylabel('Offensive EPA/Play', fontsize=11)
ax1.set_title('4-Year EPA Trajectory', fontsize=12, fontweight='bold')
ax1.axhline(y=0.15, color='green', linestyle='--', label='Top 25 Threshold')
ax1.legend(fontsize=9)
for bar, val in zip(bars, epa):
ax1.text(bar.get_x() + bar.get_width()/2, val + 0.005,
f'{val:.2f}', ha='center', fontsize=10, fontweight='bold')
# Chart 2: Win correlation
ax2 = axes[1]
teams_epa = np.random.uniform(0, 0.25, 130)
teams_wins = 3 + teams_epa * 35 + np.random.normal(0, 1.5, 130)
teams_wins = np.clip(teams_wins, 0, 12)
ax2.scatter(teams_epa, teams_wins, alpha=0.3, color='gray', s=30)
ax2.scatter([0.22], [11], color='#CC0033', s=150, zorder=5,
label='State University')
# Trend line
z = np.polyfit(teams_epa, teams_wins, 1)
p = np.poly1d(z)
x_line = np.linspace(0, 0.25, 100)
ax2.plot(x_line, p(x_line), 'b--', alpha=0.5)
ax2.set_xlabel('Offensive EPA/Play', fontsize=11)
ax2.set_ylabel('Wins', fontsize=11)
ax2.set_title('EPA vs. Wins (FBS)', fontsize=12, fontweight='bold')
ax2.legend(fontsize=9)
# Chart 3: Conference ranking
ax3 = axes[2]
categories = ['Offense\nEPA', 'Defense\nEPA', 'Special\nTeams', 'Overall']
ranks = [2, 3, 4, 2]
bars = ax3.bar(categories, ranks, color='#CC0033')
ax3.set_ylabel('Conference Rank', fontsize=11)
ax3.set_title('Conference Standings', fontsize=12, fontweight='bold')
ax3.set_ylim(0, 15)
ax3.invert_yaxis() # Lower rank is better
for bar, rank in zip(bars, ranks):
ax3.text(bar.get_x() + bar.get_width()/2, rank + 0.3,
f'#{rank}', ha='center', fontsize=12, fontweight='bold')
plt.tight_layout()
return fig
Key Design Decisions: 1. Business metrics focus: EPA linked to wins (ROI demonstration) 2. Simple comparisons: 3 clear charts, not 15 complex ones 3. Success highlighted: School position clearly marked 4. Trend emphasis: Multi-year improvement shown 5. Minimal technical jargon: "EPA" explained in title
Comparative Analysis
Summary Table
| Aspect | Broadcast | Social Media | Coaching | Executive |
|---|---|---|---|---|
| View Time | 5-8 sec | 2-3 sec | Minutes-Hours | 2-3 min |
| Metrics Shown | 3 | 4 | 20+ | 6-8 |
| Font Size (min) | 16pt | 14pt | 8pt | 10pt |
| Color Complexity | Low | Medium | High | Medium |
| Interactivity | None | None | Links to film | Click-through |
| Emotional Appeal | Medium | High | Low | Medium |
| Technical Depth | Surface | Surface | Deep | Summary |
| Call to Action | Watch game | Engage | Prepare | Approve budget |
Color Palette Adaptations
# Same semantic meaning, different execution
color_schemes = {
'broadcast': {
'background': '#1a1a2e', # Dark for TV contrast
'primary': '#ffffff',
'accent': 'team_colors'
},
'social': {
'background': '#ffffff', # Clean for small screens
'primary': '#333333',
'accent': 'bright_saturated'
},
'coaching': {
'background': '#ffffff', # Print-friendly
'primary': '#000000',
'accent': 'muted_functional'
},
'executive': {
'background': '#ffffff', # Professional
'primary': '#333333',
'accent': 'brand_colors'
}
}
Key Lessons
1. Same Data, Different Stories
The underlying data was identical, but each visualization told a different story: - Broadcast: "This is an exciting matchup" - Social: "Who will win?" - Coaching: "Here's how to beat them" - Executive: "Analytics drives wins"
2. Audience Constraints Drive Design
Every design decision flowed from audience constraints: - Time available β Information density - Viewing context β Typography choices - User goals β Which metrics to highlight - Technical literacy β Terminology level
3. Simplification Requires Expertise
Knowing what to leave out is harder than including everything. The broadcast graphic showing only 3 stats required understanding which 3 were most meaningful.
4. Brand Consistency Across Contexts
Despite different designs, brand elements remained consistent: - School colors appeared in all versions - Logo placement followed guidelines - Typography family remained consistent
Discussion Questions
-
How would you adapt these visualizations for a radio broadcast (audio only)?
-
What additional audience segment might benefit from customized visualizations?
-
How do you balance data accuracy with emotional appeal in fan-facing content?
-
What ethical considerations arise when simplifying data for different audiences?
-
How would you measure the effectiveness of each visualization for its intended purpose?
Practice Exercise
Using the matchup data provided at the beginning of this case study, create your own visualization for a fifth audience: High School Recruits and Their Families.
Consider: - What do recruits care about most? - How sophisticated are they with football analytics? - What emotional elements should be included? - What's the appropriate balance of program success vs. player development metrics?
Document your design decisions and justify each choice based on audience analysis.