[Python + Canva] Narrative Visualization for Policy Advocacy
When you write a policy brief, you’re making an argument backed by evidence. Data visualization is one of your most powerful tools for making that argument memorable and persuasive. This post introduces narrative visualization and shows you how to create publication-ready figures for policy documents.
Types of Documents for Policy Advoacy
Before diving into visualization, let’s clarify the types of documents you’ll encounter in social work practice. These terms often overlap, and different organizations use them inconsistently. The names matter less than understanding the underlying dimension: how much detail does your audience need?
- A one-pager (also called a fact sheet or issue brief) is exactly what it sounds like: a single page that distills your argument to its essential elements.
- One-pagers are common in legislative advocacy because staffers and legislators are constantly context-switching between issues. If you can’t explain your ask in one page, you probably won’t get the meeting.
- Typical structure: a headline finding, 2-3 supporting data points with visualizations, and a clear “ask” or recommendation.
- A policy brief is slightly longer (typically 2-8 pages) in general and allows for more context. According to the UNC Writing Center, it “presents a concise summary of information that can help readers understand, and likely make decisions about, government policies.”
- You have room to explain the problem, present evidence, and make specific recommendations. Policy briefs still prioritize brevity over comprehensiveness; the goal is to inform a decision, not to document all possible nuances.
- Please find this resource by IHPI at U of Michigan: https://ihpi.umich.edu/sites/default/files/2025-09/CreatingaOnePager.pdf
- A white paper is longer and more detailed (often 10-30 pages). It provides in-depth analysis of a problem, presents comprehensive research, and proposes solutions.
- The term originated with British government documents in the 1920s (the Churchill White Paper of 1922 is an early example).
- Unlike policy briefs, white papers allow for methodological detail, literature review, and extended discussion of tradeoffs. They target readers who want to understand the full picture before making decisions.
In practice, you’ll hear these terms used loosely. Someone might call their 3-page document a “one-pager” or their 6-page document a “white paper.” Don’t get hung up on terminology. Focus on matching your document length and depth to your audience’s needs and attention span.
Both documents rely heavily on data visualization to communicate findings quickly. When a legislator has 5 minutes to understand your issue, a well-designed chart can do what 3 paragraphs of text cannot.
The Narrative Arc of Policy Visualization
Segel and Heer (2010) studied how effective data visualizations guide readers through information. They found that the best visualizations balance author-driven structure (guiding readers through a specific narrative) with reader-driven exploration (letting the audience investigate details that matter to them). For policy briefs, you’ll lean more toward author-driven. You have a specific point to make, and your job is to make it clearly and honestly.
Most effective policy visualizations follow a structure similar to storytelling:
- Setup: Establish the baseline or context. What’s normal? What should the audience expect?
- Complication: Introduce the problem. Show the gap, the disparity, the trend that concerns you.
- Resolution: Point toward what this means or what should happen next.
Example
Let’s work with real data from the USDA. In fiscal year 2024, the Supplemental Nutrition Assistance Program (SNAP) served about 41.2 million people monthly. But participation rates vary dramatically by state: from 21.2% in New Mexico to just 4.8% in Utah.

The USDA originally visualizes this data as a choropleth map (the colored map you see on their website). Maps are good for showing geographic patterns, but they have limitations: it’s hard to compare exact values, small states like Rhode Island are barely visible, and large states like Alaska dominate visually even when their population is small.
You can download the data here: https://www.ers.usda.gov/media/1159/stateparticipationratemap.xlsx
Let’s first load the data.
### 1. DATA LOADING
import pandas as pd
import matplotlib.pyplot as plt
# Load the Excel file (skip the header row)
df = pd.read_excel('state_participation_rate_map.xlsx', skiprows=1)
# Rename columns for easier use
df.columns = ['State', 'SNAP_Rate']
# Convert rate to numeric (some cells may have formatting issues)
df['SNAP_Rate'] = pd.to_numeric(df['SNAP_Rate'], errors='coerce')
# Remove any rows with missing data
df = df.dropna()
# Sort by participation rate (lowest to highest)
df = df.sort_values('SNAP_Rate', ascending=True)
print(df.head(10))
print(f"\nTotal states: {len(df)}")
print(f"Range: {df['SNAP_Rate'].min()}% to {df['SNAP_Rate'].max()}%")For a policy brief focused on ranking states or highlighting specific disparities, a bar chart is often more effective. Let’s transform the USDA’s map data into a bar chart that makes comparisons easier. Let’s create a horizontal bar chart showing every state ranked by SNAP participation rate:
### 2. BAR CHART
# Create figure - taller to accommodate all 51 states
fig, ax = plt.subplots(figsize=(10, 14))
# Calculate the national average
national_avg = df['SNAP_Rate'].mean()
# Create horizontal bar chart
bars = ax.barh(df['State'], df['SNAP_Rate'], color='#1696d2', height=0.7)
# Label the axes
ax.set_xlabel('SNAP Participation Rate (%)', fontsize=11)
ax.set_title('SNAP Participation Rates by State, FY 2024',
fontweight='bold', fontsize=13)
ax.set_xlim(0, 25)
# Add reference line for national average
ax.axvline(x=national_avg, color='#ec008b', linestyle='--', linewidth=1.5)
ax.text(national_avg + 0.3, 48, f'National avg: {national_avg:.1f}%',
fontsize=9, color='#ec008b')
# Clean up the appearance
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
plt.savefig('snap_all_states.png', dpi=300, bbox_inches='tight')
plt.show()
This chart shows every state, but 51 bars can feel overwhelming. Sometimes a more focused approach works better for policy communication.
💫 Let’s be more strategic! For a one-pager or executive summary, you might want to highlight just the extremes. This draws attention to the gap without overwhelming readers.
One way to enrich your visualization is to add contextual layers that help readers interpret the data. For example, you might color-code by region, poverty rate quartile, or political leaning. Adding context invites readers to ask “why” and can strengthen your policy argument. Here, we’ll add 2024 presidential election results as one possible contextual layer:
# 2024 Presidential Election results by state
# Source: AP/major news outlets, November 2024
election_2024 = {
'Alabama': 'R', 'Alaska': 'R', 'Arizona': 'R', 'Arkansas': 'R',
'California': 'D', 'Colorado': 'D', 'Connecticut': 'D', 'Delaware': 'D',
'District of Columbia': 'D', 'Florida': 'R', 'Georgia': 'R', 'Hawaii': 'D',
'Idaho': 'R', 'Illinois': 'D', 'Indiana': 'R', 'Iowa': 'R',
'Kansas': 'R', 'Kentucky': 'R', 'Louisiana': 'R', 'Maine': 'D',
'Maryland': 'D', 'Massachusetts': 'D', 'Michigan': 'R', 'Minnesota': 'D',
'Mississippi': 'R', 'Missouri': 'R', 'Montana': 'R', 'Nebraska': 'R',
'Nevada': 'R', 'New Hampshire': 'D', 'New Jersey': 'D', 'New Mexico': 'D',
'New York': 'D', 'North Carolina': 'R', 'North Dakota': 'R', 'Ohio': 'R',
'Oklahoma': 'R', 'Oregon': 'D', 'Pennsylvania': 'R', 'Rhode Island': 'D',
'South Carolina': 'R', 'South Dakota': 'R', 'Tennessee': 'R', 'Texas': 'R',
'Utah': 'R', 'Vermont': 'D', 'Virginia': 'D', 'Washington': 'D',
'West Virginia': 'R', 'Wisconsin': 'R', 'Wyoming': 'R'
}
# Add political leaning to dataframe
df['Party_2024'] = df['State'].map(election_2024)
# Select top 10 and bottom 10 states
bottom_10 = df.head(10) # Lowest rates
top_10 = df.tail(10) # Highest rates
# Combine them
highlight_df = pd.concat([bottom_10, top_10])
# Create the focused chart with extra space on the right for annotation
fig, ax = plt.subplots(figsize=(12, 8))
# Color bars by actual 2024 election results
colors = ['#B22234' if party == 'R' else '#3C3B6E'
for party in highlight_df['Party_2024']]
bars = ax.barh(highlight_df['State'], highlight_df['SNAP_Rate'],
color=colors, height=0.7)
# Add white percentage labels on top of each bar
for bar, rate in zip(bars, highlight_df['SNAP_Rate']):
ax.text(bar.get_width() - 0.5, bar.get_y() + bar.get_height()/2,
f'{rate:.1f}%', va='center', ha='right',
fontsize=9, fontweight='bold', color='white')
ax.set_xlabel('SNAP Participation Rate (%)', fontsize=11)
ax.set_title('States with Highest and Lowest SNAP Participation Rates\n(colored by 2024 presidential election results)',
fontweight='bold', fontsize=12)
ax.set_xlim(0, 30) # Extended to make room for gap annotation
# Add reference line for national average
national_avg = df['SNAP_Rate'].mean()
ax.axvline(x=national_avg, color='#555555', linestyle='--', linewidth=1.5)
ax.text(national_avg + 0.3, 19.5, f'National avg:\n{national_avg:.1f}%',
fontsize=9, color='#555555')
# Add gap annotation on the right side
max_rate = highlight_df['SNAP_Rate'].max()
min_rate = highlight_df['SNAP_Rate'].min()
gap = max_rate - min_rate
# Draw bracket on the right
bracket_x = 26
ax.plot([bracket_x, bracket_x+0.5, bracket_x+0.5, bracket_x],
[0, 0, 19, 19], color='#333333', lw=2)
ax.text(bracket_x + 1, 9.5, f'{gap:.1f} pp\ngap',
va='center', ha='left', fontsize=11, fontweight='bold', color='#333333')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
plt.savefig('snap_top_bottom.png', dpi=300, bbox_inches='tight')
plt.show()
Notice how adding color reveals patterns you might not see otherwise. The visualization doesn’t make causal claims, but it invites questions: Why do some red states have high participation while others don’t? What policies differ between states? This is the power of contextual layering in policy visualization.
Let me walk through what each part does:
fig, ax = plt.subplots(figsize=(10, 8)) creates a figure that’s 10 inches wide and 8 inches tall. The function returns two objects: fig (the overall canvas) and ax (the plotting area where we draw our chart).
ax.barh() creates a horizontal bar chart. Horizontal bars are better than vertical when you have long category labels (state names) because the text reads naturally left-to-right.
The color list ['#1696d2'] * 10 + ['#ec008b'] * 10 creates a list with 10 blue values followed by 10 pink values. This visually separates the low-participation states from the high-participation states.
ax.axvline() adds a vertical reference line at the national average. This gives readers a benchmark to compare each state against.
ax.spines controls the chart borders. Removing the top and right spines creates a cleaner look.
plt.tight_layout() automatically adjusts spacing so labels don’t get cut off. Always call this before saving.
Multi-Panel Figures for Complex Narratives
Sometimes one chart isn’t enough. A policy brief might need multiple visualizations that work together to tell a complete story. Instead of cramming everything into one busy chart, you can create a multi-panel figure.
# Create a figure with two side-by-side panels
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# Panel 1: Distribution of rates (histogram)
ax1 = axes[0]
ax1.hist(df['SNAP_Rate'], bins=10, color='#1696d2', edgecolor='white')
ax1.axvline(x=df['SNAP_Rate'].mean(), color='#ec008b', linestyle='--', linewidth=2)
ax1.set_xlabel('SNAP Participation Rate (%)')
ax1.set_ylabel('Number of States')
ax1.set_title('Distribution of State SNAP Rates', fontweight='bold')
ax1.text(df['SNAP_Rate'].mean() + 0.5, 8, f'Mean: {df["SNAP_Rate"].mean():.1f}%',
color='#ec008b', fontsize=10)
# Panel 2: Highlight the extremes
ax2 = axes[1]
extreme_states = df[df['State'].isin(['New Mexico', 'Utah', 'Louisiana',
'District of Columbia', 'Wyoming'])]
extreme_states = extreme_states.sort_values('SNAP_Rate')
colors = ['#1696d2' if rate < 10 else '#ec008b'
for rate in extreme_states['SNAP_Rate']]
ax2.barh(extreme_states['State'], extreme_states['SNAP_Rate'], color=colors)
ax2.set_xlabel('SNAP Participation Rate (%)')
ax2.set_title('The Extremes: A 4x Difference', fontweight='bold')
ax2.set_xlim(0, 25)
# Clean up both panels
for ax in axes:
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
plt.savefig('snap_multipanel.png', dpi=300, bbox_inches='tight')
plt.show()When you call plt.subplots(1, 2), you’re asking for 1 row and 2 columns of subplots. The axes variable becomes an array, so axes[0] is the left panel and axes[1] is the right panel. You can then customize each panel independently while keeping them visually unified.

This mirrors the narrative arc: Panel 1 provides context (setup), Panel 2 highlights the disparity (complication). The title “The Gap: 16.4 Percentage Points” encodes the takeaway directly, following Segel and Heer’s concept of messaging in visualization.
Exporting Publication-Quality Figures
The savefig() function creates your image file. Understanding its parameters is important for producing professional output.
dpi=300 sets the resolution (dots per inch). For print publications and policy briefs, 300 dpi is standard. For screen-only display, 150 dpi is usually sufficient. Higher DPI means larger file sizes but sharper output.
bbox_inches='tight' automatically crops the figure to remove excess white space while keeping all labels visible. Without this, matplotlib sometimes cuts off axis labels or titles.
transparent=True removes the white background. This is essential when you’re placing figures into design tools like Canva where you want the figure to blend with your document’s background color.
# Different export options for different uses
# For design tools like Canva (transparent background, high resolution)
plt.savefig('figure_for_canva.png', dpi=300, bbox_inches='tight',
transparent=True, facecolor='none')
# For print publications or PDFs
plt.savefig('figure_print.png', dpi=300, bbox_inches='tight')
# For web display only (smaller file size)
plt.savefig('figure_web.png', dpi=150, bbox_inches='tight')
# Vector format for editing in Adobe Illustrator
plt.savefig('figure_editable.svg', bbox_inches='tight')Maintaining Visual Consistency
One of the most important lessons from professional policy organizations is the value of visual consistency. The Urban Institute maintains a comprehensive Data Visualization Style Guide that specifies exact colors, fonts, and layout rules for all their publications. You can also search for branding guide of your own institution. U of Michigan has one too.
According to Jonathan Schwabish, a Senior Fellow at Urban who helped develop the guide, this consistency “reduces the burden of design and color decisions on individual researchers” while ensuring that all Urban publications are instantly recognizable. Their style guide explicitly states that bar charts should always start axes at zero, avoiding visual exaggeration of differences.
For your own work, define your style choices once at the top of your notebook and reuse them:
# Define project colors once, reuse everywhere
PROJECT_COLORS = {
'primary': '#1696d2', # Urban Institute cyan
'secondary': '#fdbf11', # Yellow
'highlight': '#ec008b', # Magenta for emphasis
'neutral_dark': '#353535', # For text
'neutral_light': '#9d9d9d' # For secondary elements
}
# Consistent sizing
TITLE_SIZE = 13
LABEL_SIZE = 11
TICK_SIZE = 10
# Apply to every figure
fig, ax = plt.subplots(figsize=(10, 6))
ax.set_title('Your Title', fontsize=TITLE_SIZE, fontweight='bold')
ax.set_xlabel('X Label', fontsize=LABEL_SIZE)
ax.tick_params(labelsize=TICK_SIZE)Canva: Assembling Documents

For design of policy brief/white paper, I recommend using Canva instead of MS Word or Google Docs. You can first create individual figures in Python, export as high-resolution PNGs with transparent backgrounds. Then, assemble the final document in a design tool like Canva.
Canva is free, web-based, and has templates specifically designed for policy briefs and reports. Search “policy brief” or “white paper” in the template library. When you export your Python figures with transparent=True, they’ll blend with whatever background color or design elements the template uses.
Using LLMs to Improve Your Policy Writing
Large language models can be useful writing assistants for policy documents. Research on LLM-assisted writing is still emerging, but several applications show promise for policy communication.
Readability Modification
One well-studied application is text simplification. A study comparing prompting strategies for simplifying academic texts found that prompts incorporating specific readability metrics (like Flesch-Kincaid Grade Level) produced significantly more readable output than generic “make this simpler” instructions (ResearchGate, 2025). The key insight: being specific about how you want text modified produces better results than vague requests.
Research by Trott (2024) found that LLMs can reliably estimate text readability and modify text to target different reading levels. However, he notes an important tradeoff: simplification can lose information. When using LLMs to simplify policy language, always check that the simplified version still conveys your key points accurately.
Prompting guide: Education researchers have developed structured frameworks for writing effective prompts. One widely-cited framework is TRACI (Park & Choo, 2024):
- Task: What do you want the LLM to do?
- Role: What perspective should it take?
- Audience: Who is the intended reader?
- Create: What format or constraints?
- Intent: What’s the purpose?
For policy writing, specifying the audience is particularly important. “Explain SNAP eligibility” will produce very different output than “Explain SNAP eligibility to a state legislator who has 2 minutes to read this” or “Explain SNAP eligibility to a client who reads at a 6th grade level.”
Practical Applications for Policy Documents
Here are specific ways to use LLMs in your policy writing workflow:
Readability Check. Paste your draft and ask:
"Analyze the reading level of this text using Flesch-Kincaid Grade Level. Identify any sentences that are longer than 25 words or use jargon that a general audience might not understand. Suggest specific revisions."
Audience Adaptation. The same content often needs multiple versions:
"Here is my executive summary written for researchers. Rewrite it for: (1) a state legislator who has 90 seconds, (2) a community newsletter for SNAP recipients. Keep the core message but adjust vocabulary and length."
Figure Caption Writing. Captions should stand alone since readers often skim figures first:
"Write a caption for a bar chart showing SNAP participation rates by state. The main finding is that Southern states have higher participation rates. The caption should explain what the reader is seeing and state the key takeaway in 2-3 sentences."
Clarity and Logic Check. Before finalizing:
"Read this policy brief. Identify: (1) any claims that aren't supported by the data I've presented, (2) logical gaps in my argument, (3) sentences that are unclear or could be misinterpreted."
Plain Language Translation. For technical terms:
"Replace the following jargon with plain language alternatives that a general audience would understand: 'means-tested entitlement program,' 'categorical eligibility,' 'benefit cliff.'"
Resources
Policy Writing:
Style Guides:
Interactive Storytelling:
Data Sources:
