{"spec_id":"bullet-basic","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nbullet-basic: Basic Bullet Chart\nLibrary: plotnine 0.15.4 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-05-29\n\"\"\"\n\nimport os\n\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    annotate,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_rect,\n    geom_segment,\n    geom_text,\n    geom_tile,\n    ggplot,\n    guides,\n    labs,\n    scale_fill_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\n\n\n# Theme-adaptive chrome tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — brand green is always first series\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nBRAND = IMPRINT_PALETTE[0]  # #009E73 — actual value bar\n\n# Grayscale bands: darker = worse performance zone (theme-adaptive for readability)\nif THEME == \"light\":\n    band_colors = {\"Poor\": \"#686868\", \"Satisfactory\": \"#9E9E9E\", \"Good\": \"#C8C8C8\"}\nelse:\n    band_colors = {\"Poor\": \"#383838\", \"Satisfactory\": \"#555555\", \"Good\": \"#727272\"}\n\n# Data — four business KPIs with mixed above/below-target performance\nmetrics = [\n    {\"label\": \"Revenue ($K)\", \"actual\": 275, \"target\": 250, \"ranges\": [150, 225, 300]},\n    {\"label\": \"Profit (%)\", \"actual\": 22, \"target\": 26, \"ranges\": [15, 22.5, 30]},\n    {\"label\": \"New Orders\", \"actual\": 1050, \"target\": 1100, \"ranges\": [600, 900, 1200]},\n    {\"label\": \"Satisfaction\", \"actual\": 4.5, \"target\": 4.2, \"ranges\": [2.5, 3.5, 5.0]},\n]\n\n# Build dataframes — all values normalized to 0–100 scale for aligned x-axis\ntile_data = []\nactual_data = []\ntarget_data = []\n\nrange_height = 0.68\nactual_height = 0.28\n\nfor i, m in enumerate(metrics):\n    y_pos = len(metrics) - 1 - i  # first metric at top\n    max_val = m[\"ranges\"][-1]\n\n    band_bounds = [0, (m[\"ranges\"][0] / max_val) * 100, (m[\"ranges\"][1] / max_val) * 100, 100]\n    band_names = [\"Poor\", \"Satisfactory\", \"Good\"]\n    for j, band in enumerate(band_names):\n        x_center = (band_bounds[j] + band_bounds[j + 1]) / 2\n        width = band_bounds[j + 1] - band_bounds[j]\n        tile_data.append({\"y\": y_pos, \"x\": x_center, \"width\": width, \"band\": band})\n\n    actual_pct = (m[\"actual\"] / max_val) * 100\n    val = m[\"actual\"]\n    val_str = str(int(val)) if val == int(val) else str(val)\n    actual_data.append(\n        {\n            \"y\": y_pos,\n            \"xmin\": 0,\n            \"xmax\": actual_pct,\n            \"ymin\": y_pos - actual_height / 2,\n            \"ymax\": y_pos + actual_height / 2,\n            \"label\": m[\"label\"],\n            \"actual\": val_str,\n            \"label_y\": y_pos + range_height / 2 + 0.04,\n        }\n    )\n\n    target_pct = (m[\"target\"] / max_val) * 100\n    target_data.append(\n        {\n            \"y\": y_pos,\n            \"target\": target_pct,\n            \"seg_ymin\": y_pos - range_height / 2.2,\n            \"seg_ymax\": y_pos + range_height / 2.2,\n        }\n    )\n\ndf_tiles = pd.DataFrame(tile_data)\ndf_actual = pd.DataFrame(actual_data)\ndf_target = pd.DataFrame(target_data)\n\n# Band label x positions: 3 of 4 metrics share 50%/75% band boundaries\npoor_mid = 25.0\nsatis_mid = 62.5\ngood_mid = 87.5\n\nplot = (\n    ggplot()\n    # Qualitative range bands\n    + geom_tile(df_tiles, aes(x=\"x\", y=\"y\", width=\"width\", fill=\"band\"), height=range_height, color=\"none\")\n    + scale_fill_manual(values=band_colors, limits=[\"Good\", \"Satisfactory\", \"Poor\"])\n    + guides(fill=False)\n    # Actual value bar\n    + geom_rect(df_actual, aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\"), fill=BRAND, color=\"none\")\n    # Target marker — thin contrasting line perpendicular to the bar\n    + geom_segment(df_target, aes(x=\"target\", xend=\"target\", y=\"seg_ymin\", yend=\"seg_ymax\"), color=INK, size=2.0)\n    # Actual value labels (geom_text size in mm; ~3.5 ≈ 10pt at 400dpi)\n    + geom_text(\n        df_actual,\n        aes(x=\"xmax\", y=\"label_y\", label=\"actual\"),\n        ha=\"right\",\n        va=\"bottom\",\n        size=3.5,\n        color=BRAND,\n        fontweight=\"bold\",\n    )\n    # Band zone labels below the bottom metric\n    + annotate(\"text\", x=poor_mid, y=-0.5, label=\"Poor\", size=3.4, color=INK_MUTED, va=\"top\")\n    + annotate(\"text\", x=satis_mid, y=-0.5, label=\"Satisfactory\", size=3.4, color=INK_MUTED, va=\"top\")\n    + annotate(\"text\", x=good_mid, y=-0.5, label=\"Good\", size=3.4, color=INK_MUTED, va=\"top\")\n    # Scales\n    + scale_x_continuous(limits=(0, 100), breaks=[0, 25, 50, 75, 100], expand=(0, 0.02))\n    + scale_y_continuous(\n        breaks=list(range(len(metrics))), labels=[m[\"label\"] for m in reversed(metrics)], expand=(0.08, 0.08)\n    )\n    + labs(title=\"bullet-basic · python · plotnine · anyplot.ai\", x=\"Performance (%)\", y=\"\")\n    + theme_minimal()\n    + theme(\n        figure_size=(8, 4.5),\n        plot_title=element_text(size=12, ha=\"center\", weight=\"bold\", color=INK),\n        axis_title_x=element_text(size=10, color=INK),\n        axis_title_y=element_blank(),\n        axis_text_x=element_text(size=8, color=INK_SOFT),\n        axis_text_y=element_text(size=9, ha=\"right\", color=INK_SOFT),\n        panel_grid_major_y=element_blank(),\n        panel_grid_minor=element_blank(),\n        panel_grid_major_x=element_line(color=INK, size=0.3, alpha=0.15),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG, color=\"none\"),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_text=element_text(color=INK_SOFT),\n    )\n)\n\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\", verbose=False)\n"}