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.
Example Visualization
.png&w=1920&q=75)
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.
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
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.