Basic Bullet Chart — Pygal

A bullet chart displays a single measure against qualitative ranges and a target marker, designed by Stephen Few as a space-efficient alternative to gauge charts. The linear format shows actual performance as a bar, a target as a vertical marker, and background bands representing qualitative ranges (poor/satisfactory/good). Its compact design allows multiple bullet charts to fit on a single dashboard for easy comparison across metrics.

Basic Bullet Chart rendered with Pygal

Python source (Pygal)

""" anyplot.ai
bullet-basic: Basic Bullet Chart
Library: pygal 3.1.0 | Python 3.13.13
Quality: 86/100 | Updated: 2026-05-29
"""

import importlib.util
import os
import sys
import xml.etree.ElementTree as ET

import cairosvg


# Guard against self-import: this file is named pygal.py, so a plain
# `import pygal` picks up itself instead of the installed package.
pygal_spec = importlib.util.find_spec("pygal")
if pygal_spec and pygal_spec.origin != __file__:
    import pygal
    from pygal.style import Style
else:
    _cwd = os.getcwd()
    sys.path = [p for p in sys.path if os.path.abspath(p) != os.path.abspath(_cwd)]
    try:
        import pygal
        from pygal.style import Style
    finally:
        sys.path.insert(0, _cwd)

THEME = os.getenv("ANYPLOT_THEME", "light")

# Theme-adaptive chrome — Imprint palette
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

# Qualitative range band colors: warm-neutral grays, theme-adaptive
if THEME == "light":
    BAND_POOR = "#E4E3DC"
    BAND_SAT = "#C0BFB8"
    BAND_GOOD = "#8E8D87"
else:
    BAND_POOR = "#3A3A30"
    BAND_SAT = "#484843"
    BAND_GOOD = "#60605A"

# Imprint semantic anchors: green for at/above target, red for below
COLOR_ABOVE = "#009E73"  # brand green — at or above target
COLOR_BELOW = "#AE3030"  # matte red — below target
COLOR_TARGET = INK  # theme-adaptive neutral for target marker

# Manufacturing & supply chain KPIs (7 metrics across ops domains)
metrics = [
    {"label": "On-Time Delivery", "actual": 91, "target": 95, "max": 100, "fmt": "{}%"},
    {"label": "First Pass Yield", "actual": 88, "target": 90, "max": 100, "fmt": "{}%"},
    {"label": "OEE", "actual": 72, "target": 85, "max": 100, "fmt": "{}%"},
    {"label": "Inventory Turnover", "actual": 8, "target": 10, "max": 12, "fmt": "{}x"},
    {"label": "Supplier On-Time", "actual": 87, "target": 90, "max": 100, "fmt": "{}%"},
    {"label": "Capacity Utilization", "actual": 82, "target": 80, "max": 100, "fmt": "{}%"},
    {"label": "Quality Score", "actual": 94, "target": 92, "max": 100, "fmt": "{}/100"},
]

POOR_PCT = 50
SAT_PCT = 75

actual_pcts = [round((m["actual"] / m["max"]) * 100, 1) for m in metrics]
target_pcts = [round((m["target"] / m["max"]) * 100, 1) for m in metrics]
above_target = [m["actual"] >= m["target"] for m in metrics]
labels = [f"{m['label']} ({m['fmt'].format(m['actual'])})" for m in metrics]

custom_style = Style(
    background=PAGE_BG,
    plot_background=PAGE_BG,
    foreground=INK,
    foreground_strong=INK,
    foreground_subtle=INK_MUTED,
    font_family="DejaVu Sans, Helvetica, Arial, sans-serif",
    colors=(COLOR_ABOVE, COLOR_BELOW, COLOR_TARGET, BAND_POOR, BAND_SAT, BAND_GOOD),
    title_font_size=66,
    label_font_size=56,
    major_label_font_size=44,
    legend_font_size=44,
    value_font_size=36,
    stroke_width=2.5,
)

chart = pygal.HorizontalStackedBar(
    width=3200,
    height=1800,
    title="bullet-basic · python · pygal · anyplot.ai",
    style=custom_style,
    show_legend=True,
    legend_at_bottom=True,
    legend_box_size=26,
    print_values=False,
    print_zeroes=False,
    show_y_guides=False,
    show_x_guides=True,
    margin=40,
    spacing=0,
    rounded_bars=2,
    truncate_label=-1,
    x_title="Performance (% of Maximum)",
    range=(0, 100),
)
chart.x_labels = labels

# Legend-only placeholder series (serie-0/1/2): first series gets brand green (#009E73)
chart.add("Above Target", [None] * len(metrics))
chart.add("Below Target", [None] * len(metrics))
chart.add("Target", [None] * len(metrics))

# Background bands: stacked segments for Poor / Satisfactory / Good zones (serie-3/4/5)
chart.add("Poor (0–50%)", [{"value": POOR_PCT, "label": lbl} for lbl in labels])
chart.add("Satisfactory (50–75%)", [{"value": SAT_PCT - POOR_PCT, "label": lbl} for lbl in labels])
chart.add("Good (75–100%)", [{"value": 100 - SAT_PCT, "label": lbl} for lbl in labels])

# Parse rendered SVG for coordinate-space injection of actual bars and markers
ET.register_namespace("", "http://www.w3.org/2000/svg")
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
svg_bytes = chart.render()
root = ET.fromstring(svg_bytes)
NS = "http://www.w3.org/2000/svg"

parent_map = {child: parent for parent in root.iter() for child in parent}

# Make all stroked lines solid — style guide requires solid reference lines
for elem in root.iter():
    if "stroke-dasharray" in elem.attrib:
        del elem.attrib["stroke-dasharray"]

# serie-3 (Poor band) rects serve as the coordinate reference for injection
serie_0 = next((g for g in root.iter(f"{{{NS}}}g") if "serie-3" in g.get("class", "")), None)

poor_bars = []
if serie_0 is not None:
    for rect in serie_0.iter(f"{{{NS}}}rect"):
        w, h = float(rect.get("width", "0")), float(rect.get("height", "0"))
        if w > 1 and h > 1:
            poor_bars.append((float(rect.get("x")), float(rect.get("y")), w, h))

inject_parent = parent_map.get(serie_0, root)
for i, (bx, by, bw, bh) in enumerate(poor_bars):
    px_per_pct = bw / POOR_PCT
    cy = by + bh / 2

    # Actual value bar at 42% of band height (classic bullet chart layering)
    actual_w = actual_pcts[i] * px_per_pct
    bar_h = bh * 0.42
    bar_color = COLOR_ABOVE if above_target[i] else COLOR_BELOW
    a = ET.SubElement(inject_parent, f"{{{NS}}}rect")
    a.set("x", f"{bx:.1f}")
    a.set("y", f"{cy - bar_h / 2:.1f}")
    a.set("width", f"{actual_w:.1f}")
    a.set("height", f"{bar_h:.1f}")
    a.set("fill", bar_color)
    a.set("rx", "2")

    # Target marker: prominent vertical rectangle at target percentage
    tx = bx + target_pcts[i] * px_per_pct
    marker_h = bh * 0.75
    t = ET.SubElement(inject_parent, f"{{{NS}}}rect")
    t.set("x", f"{tx - 6:.1f}")
    t.set("y", f"{cy - marker_h / 2:.1f}")
    t.set("width", "12")
    t.set("height", f"{marker_h:.1f}")
    t.set("fill", COLOR_TARGET)

svg_modified = ET.tostring(root, encoding="utf-8")

# Save PNG at canonical 3200x1800 and interactive HTML
cairosvg.svg2png(bytestring=svg_modified, write_to=f"plot-{THEME}.png")
with open(f"plot-{THEME}.html", "wb") as f:
    f.write(svg_modified)

Part of Basic Bullet Chart on anyplot.ai.

Other implementations