Menu

Comparison
Static
18 Python scripts generated for bullet graph this week

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

Bullet graph showing revenue, EBITDA, and net profit against targets with performance ranges

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
Example AI 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

1

Upload Data

Drag & drop your Excel or CSV file. Plotivy securely processes it in your browser.

2

AI Generation

Our AI analyzes your data and generates the Bullet Graph code automatically.

3

Customize & Export

Tweak the design with natural language, then export as high-res PNG, SVG or PDF.

Python Code Example

example.py
# === 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-CODE

Opens the Analyze page with this code pre-loaded and ready to execute

Console Output

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

Free Cheat Sheet

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.

Comparison Charts
Distribution Charts
Time Series Data
Common Mistakes
No spam. Unsubscribe anytime.