Bullet Graph
Chart overview
Bullet graphs, designed by Stephen Few as an alternative to gauges and meters, efficiently display key performance indicators (KPIs) by showing a primary measure against target values within qualitative ranges (poor, satisfactory, good).
Key points
- This compact visualization replaces dashboard widgets while conveying rich information about actual vs.
- target performance.
- Bullet graphs are ideal for executive dashboards and performance scorecards.
Python Tutorial
How to create a bullet graph in Python
Use the full tutorial for implementation details, troubleshooting, and chart variations in matplotlib, seaborn, and plotly.
Complete Guide to Scientific Data VisualizationExample Visualization
.png&w=1280&q=70)
Create This Chart Now
Generate publication-ready bullet graphs with AI in seconds. No coding required – just describe your data and let AI do the work.
View example prompt
"Create a professional bullet graph dashboard showing 3 key business KPIs: 'Revenue' (current: $420K, target: $500K), 'EBITDA' (current: $180K, target: $200K), and 'Net Profit' (current: $90K, target: $100K). For each metric, display three qualitative performance ranges: 'Poor' (0-25% of target), 'Satisfactory' (25-75% of target), and 'Good' (75-100%+ of target) using subtle gray shading. Show current performance as a bold horizontal bar and target as a vertical reference line. Add percentage labels showing achievement rate. Use a horizontal layout, include metric labels on the left, format values as currency, and add a subtle grid for readability."
How to create this chart in 30 seconds
Upload Data
Drag & drop your Excel or CSV file. Plotivy securely processes it in your browser.
AI Generation
Our AI analyzes your data and generates the Bullet Graph code automatically.
Customize & Export
Tweak the design with natural language, then export as high-res PNG, SVG or PDF.
Newsletter
Get one weekly tip for better bullet graphs
Join researchers receiving concise Python plotting techniques to improve chart clarity and reduce revision cycles.
Python Code Example
# === IMPORTS ===
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import FuncFormatter
# === USER-EDITABLE PARAMETERS ===
title = "Strong Performance: All Metrics Reach 'Good' Range (84-90% of Targets)" # Change: Insight-driven title summarizing key finding
x_label = "Amount ($ thousands)" # Change: x-axis label
y_label = "Business Metrics" # Change: y-axis label
poor_color = "#ff8a80" # Improved: Brighter light red-orange for Poor range
sat_color = "#ffd54f" # Improved: Brighter light yellow for Satisfactory range
good_color = "#a5d6a7" # Improved: Brighter light green for Good range
current_color = "#37474f" # Improved: Darker slate gray for Current bars
target_color = "#1976d2" # Improved: Distinct blue for Target markers
bar_width = 0.6 # Change: Relative width of range bars (0-1)
current_width_factor = 0.75 # Change: Makes Current bars slightly thinner than ranges
range_opacity = 0.4 # Change: Transparency for range backgrounds (0-1)
title_fontsize = 18 # Change: Title font size
label_fontsize = 14 # Change: Axis labels and legend font size
# === EXAMPLE DATASET GENERATION ===
# Change: Edit this DataFrame to customize your metrics, ranges, targets, and currents (all in $ thousands)
df = pd.DataFrame({
'Measure': ['Revenue', 'EBITDA', 'Net Profit'],
'Poor': [125, 40, 15],
'Satisfactory': [125, 60, 25],
'Good': [250, 100, 60],
'Target': [500, 200, 100],
'Current': [420, 180, 90]
})
# === DATA ANALYSIS & INSIGHTS ===
# Compute derived columns and print key values
try:
df['Pct'] = (df['Current'] / df['Target']) * 100
except Exception as e:
print(f"Error computing Pct: {e}")
df['Pct'] = None
try:
df['Sat_Base'] = df['Poor']
df['Good_Base'] = df['Poor'] + df['Satisfactory']
except Exception as e:
print(f"Error computing base ranges: {e}")
df['Sat_Base'] = None
df['Good_Base'] = None
def get_band(row):
if row['Current'] <= row['Poor']:
return 'Poor'
elif row['Current'] <= row['Good_Base']:
return 'Satisfactory'
else:
return 'Good'
try:
df['Band'] = df.apply(get_band, axis=1)
except Exception as e:
print(f"Error determining band: {e}")
df['Band'] = None
# Print relevant values BEFORE plotting
print("Example Dataset (in $ thousands):")
print(df[['Measure', 'Poor', 'Satisfactory', 'Good', 'Target', 'Current']].round(1).to_string(index=False))
print("\nPerformance Summary:")
for _, row in df.iterrows():
pct = row['Pct'] if pd.notna(row['Pct']) else 0
print(f"{row['Measure']}: ${row['Current']}k ({pct:.1f}% of ${row['Target']}k target) - {row['Band']} band")
avg_pct = df['Pct'].mean() if 'Pct' in df.columns else None
if avg_pct is not None:
print(f"\nAverage performance: {avg_pct:.1f}% across {len(df)} metrics")
print(f"All bands: {df['Band'].tolist()}")
# Update title with computed insight if desired
if avg_pct is not None:
title = f"Strong Performance: All {len(df)} Metrics in 'Good' Range (Avg {avg_pct:.0f}% of Targets)"
# === CREATE BULLET GRAPH ===
fig, ax = plt.subplots(figsize=(12, 6))
y_pos = np.arange(len(df))
# Poor range (stacked background)
ax.barh(y_pos, df['Poor'], height=bar_width, color=poor_color, alpha=range_opacity, label='Poor Range')
# Satisfactory range (stacked on Poor)
ax.barh(y_pos, df['Satisfactory'], left=df['Poor'], height=bar_width, color=sat_color, alpha=range_opacity, label='Satisfactory Range')
# Good range (stacked on Satisfactory)
# Adjusted to span from Good_Base to Target for a more realistic width
try:
good_width = df['Target'] - df['Good_Base']
# Ensure non-negative width; if zero or negative, use a minimal width for visibility
good_width = good_width.apply(lambda x: x if x > 0 else 1)
except Exception as e:
print(f"Error computing good range width: {e}")
good_width = pd.Series([1]*len(df))
ax.barh(y_pos, good_width, left=df['Good_Base'], height=bar_width, color=good_color, alpha=range_opacity, label='Good Range')
# Current values (overlaid prominent bars)
ax.barh(y_pos, df['Current'], height=bar_width * current_width_factor, color=current_color, alpha=1.0,
edgecolor='#000000', linewidth=2, label='Current')
# Target markers (vertical tick lines)
target_half_height = bar_width * 0.45
for i, target in enumerate(df['Target']):
ax.plot([target, target], [y_pos[i] - target_half_height, y_pos[i] + target_half_height],
color=target_color, linewidth=5, solid_capstyle='butt')
# Legend proxy for Targets
ax.plot([], [], color=target_color, linewidth=5, label='Targets')
# === FINALIZE LAYOUT ===
ax.set_xlim(0, df['Target'].max() * 1.05)
ax.set_title(title, fontsize=title_fontsize, pad=20)
ax.set_xlabel(x_label, fontsize=label_fontsize)
ax.set_ylabel(y_label, fontsize=label_fontsize)
ax.set_yticks(y_pos)
ax.set_yticklabels(df['Measure'])
ax.tick_params(axis='both', labelsize=label_fontsize - 2)
ax.xaxis.set_major_formatter(FuncFormatter(lambda x, pos: f'${x:,.0f}k'))
ax.legend(loc='upper right', bbox_to_anchor=(0.98, 0.99), framealpha=0.95, fontsize=label_fontsize - 2)
plt.tight_layout()
# Save as static PNG
fig.savefig('bullet_graph.png', dpi=300, bbox_inches='tight', facecolor='white')
plt.show()
fig.show()
# END-OF-CODEOpens the Analyze page with this code pre-loaded and ready to execute
Console Output
Example Dataset (in $ thousands):
Measure Poor Satisfactory Good Target Current
Revenue 125 125 250 500 420
EBITDA 40 60 100 200 180
Net Profit 15 25 60 100 90
Performance Summary:
Revenue: $420k (84.0% of $500k target) - Good band
EBITDA: $180k (90.0% of $200k target) - Good band
Net Profit: $90k (90.0% of $100k target) - Good band
Average performance: 88.0% across 3 metrics
All bands: ['Good', 'Good', 'Good']Common Use Cases
- 1Executive dashboards and KPI monitoring
- 2Sales performance against quotas
- 3Budget vs actual comparisons
- 4Goal tracking and progress reporting
Pro Tips
Keep to 3-4 qualitative ranges maximum
Use consistent color coding across all bullet graphs
Order metrics by importance or performance
Long-tail keyword opportunities
High-intent chart variations
Library comparison for this chart
plotly
Best for interactive hover, zoom, and web sharing when collaborators need to inspect values directly from bullet-graph figures.
matplotlib
Best when you need full control over axis formatting, annotation placement, and journal-specific styling for bullet-graph.
Scientific Chart Selection Cheat Sheet
Not sure whether to use a Violin Plot, Box Plot, or Ridge Plot? Download our single-page reference mapping the most-used scientific chart types, exactly when to use them, and the core Matplotlib/Seaborn functions.