{"spec_id":"bullet-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nbullet-basic: Basic Bullet Chart\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-29\n\"\"\"\n# ruff: noqa: F405\n\nimport os\nimport shutil\n\nimport pandas as pd\nfrom lets_plot import *\n\n\nLetsPlot.setup_html()\n\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 — semantic exception: green = above/success, red = below/failure\nABOVE_COLOR = \"#009E73\"  # brand green\nBELOW_COLOR = \"#AE3030\"  # matte red\n\n# Grayscale band shades — adjusted per theme so bands are visible on both surfaces\nif THEME == \"light\":\n    BAND_GOOD = \"#D0D0D0\"\n    BAND_SAT = \"#989898\"\n    BAND_POOR = \"#585858\"\nelse:\n    BAND_GOOD = \"#3C3C38\"\n    BAND_SAT = \"#565650\"\n    BAND_POOR = \"#707068\"\n\n# Data — Q4 2024 KPI dashboard with varied performance levels\nmetrics = [\"Revenue ($K)\", \"Profit Margin (%)\", \"Satisfaction\", \"New Customers\"]\nactual = [275, 38, 3.8, 42]\ntarget = [300, 42, 4.5, 40]\npoor = [100, 20, 2.5, 15]\nsatisfactory = [200, 35, 3.5, 30]\ngood = [350, 50, 5.0, 50]\n\nn = len(metrics)\n\n# Normalize to percentage of maximum range\nactual_pct = [actual[i] / good[i] * 100 for i in range(n)]\ntarget_pct = [target[i] / good[i] * 100 for i in range(n)]\npoor_pct = [poor[i] / good[i] * 100 for i in range(n)]\nsat_pct = [satisfactory[i] / good[i] * 100 for i in range(n)]\n\nstatus = [\"Above Target\" if actual[i] >= target[i] else \"Below Target\" for i in range(n)]\n\n# Y positions — reversed for top-to-bottom reading\ny_spacing = 0.90\ny_pos = [i * y_spacing for i in range(n - 1, -1, -1)]\nbar_h = 0.38\nnarrow_h = 0.17\nmarker_h = 0.33\n\n# Qualitative range bands (grayscale, Stephen Few convention)\nrange_rows = []\nfor i in range(n):\n    y = y_pos[i]\n    range_rows.append({\"xmin\": 0, \"xmax\": 100, \"ymin\": y - bar_h, \"ymax\": y + bar_h, \"band\": \"Good\"})\n    range_rows.append({\"xmin\": 0, \"xmax\": sat_pct[i], \"ymin\": y - bar_h, \"ymax\": y + bar_h, \"band\": \"Satisfactory\"})\n    range_rows.append({\"xmin\": 0, \"xmax\": poor_pct[i], \"ymin\": y - bar_h, \"ymax\": y + bar_h, \"band\": \"Poor\"})\ndf_ranges = pd.DataFrame(range_rows)\n\n# Actual value bars with interactive tooltips\nactual_rows = []\nfor i in range(n):\n    y = y_pos[i]\n    actual_rows.append(\n        {\n            \"xmin\": 0,\n            \"xmax\": actual_pct[i],\n            \"ymin\": y - narrow_h,\n            \"ymax\": y + narrow_h,\n            \"status\": status[i],\n            \"metric\": metrics[i],\n            \"actual_val\": f\"{actual[i]:g}\",\n            \"target_val\": f\"{target[i]:g}\",\n            \"achievement\": f\"{actual[i] / target[i] * 100:.0f}%\",\n        }\n    )\ndf_actual = pd.DataFrame(actual_rows)\n\n# Target markers\ntarget_rows = []\nfor i in range(n):\n    y = y_pos[i]\n    target_rows.append({\"x\": target_pct[i], \"y\": y - marker_h, \"xend\": target_pct[i], \"yend\": y + marker_h})\ndf_target = pd.DataFrame(target_rows)\n\n# Value annotations (actual units, beside each bar)\n# When the bar end is within 8 pp of the target marker, place annotation after the\n# marker instead of between bar-end and marker to avoid overlap.\nannot_labels = [\"$275K\", \"38%\", \"3.8\", \"42\"]\nannot_rows = []\nfor i in range(n):\n    crowd_target = actual_pct[i] < target_pct[i] and (target_pct[i] - actual_pct[i]) < 8\n    if crowd_target:\n        annot_rows.append(\n            {\"x\": target_pct[i] + 2, \"y\": float(y_pos[i]), \"label\": annot_labels[i], \"status\": status[i], \"hjust\": 0.0}\n        )\n    else:\n        annot_rows.append(\n            {\"x\": actual_pct[i] + 4, \"y\": float(y_pos[i]), \"label\": annot_labels[i], \"status\": status[i], \"hjust\": 0.0}\n        )\ndf_annot = pd.DataFrame(annot_rows)\n\n# Band legend note — dark-mode bands are lighter for Poor (more contrast on dark bg),\n# so the descriptor text must flip to avoid being factually wrong in dark render\nif THEME == \"light\":\n    band_note_text = \"Bands:  Dark = Poor  ·  Medium = Satisfactory  ·  Light = Good\"\nelse:\n    band_note_text = \"Bands:  Light = Poor  ·  Medium = Satisfactory  ·  Dark = Good\"\ndf_band_note = pd.DataFrame([{\"x\": 0, \"y\": -0.58, \"label\": band_note_text}])\n\n# Build layered bullet chart\nplot = (\n    ggplot()\n    # Qualitative range bands\n    + geom_rect(data=df_ranges, mapping=aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\", fill=\"band\"), size=0)\n    # Actual value bars\n    + geom_rect(\n        data=df_actual,\n        mapping=aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\", fill=\"status\"),\n        size=0,\n        tooltips=(\n            layer_tooltips()\n            .line(\"@{metric}\")\n            .line(\"Actual|@{actual_val}\")\n            .line(\"Target|@{target_val}\")\n            .line(\"Achievement|@{achievement}\")\n        ),\n    )\n    # Target markers — thin vertical lines in INK color\n    + geom_segment(data=df_target, mapping=aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), size=2.5, color=INK)\n    # Value annotations beside each bar\n    + geom_text(\n        data=df_annot,\n        mapping=aes(x=\"x\", y=\"y\", label=\"label\", color=\"status\"),\n        size=5,\n        hjust=0,\n        fontface=\"bold\",\n        show_legend=False,\n    )\n    # Band legend explanation\n    + geom_text(data=df_band_note, mapping=aes(x=\"x\", y=\"y\", label=\"label\"), size=4, hjust=0, color=INK_MUTED)\n    # Fill scale — bands (grey) + status (Imprint palette)\n    + scale_fill_manual(\n        values={\n            \"Good\": BAND_GOOD,\n            \"Satisfactory\": BAND_SAT,\n            \"Poor\": BAND_POOR,\n            \"Above Target\": ABOVE_COLOR,\n            \"Below Target\": BELOW_COLOR,\n        },\n        labels={\"Above Target\": \"↑ Above Target\", \"Below Target\": \"↓ Below Target\"},\n        breaks=[\"Above Target\", \"Below Target\"],\n        name=\"Performance\",\n    )\n    + scale_color_manual(values={\"Above Target\": ABOVE_COLOR, \"Below Target\": BELOW_COLOR}, guide=\"none\")\n    # Axes\n    + scale_x_continuous(name=\"Performance (%)\", limits=[0, 108], expand=[0, 1])\n    + scale_y_continuous(breaks=y_pos, labels=metrics, limits=[-0.78, 3.25], expand=[0, 0])\n    + labs(\n        title=\"bullet-basic · python · letsplot · anyplot.ai\",\n        subtitle=\"Q4 2024 Dashboard — Actual vs. Target Performance\",\n        y=\"\",\n    )\n    + theme_minimal()\n    + theme(\n        plot_title=element_text(size=16, face=\"bold\", color=INK),\n        plot_subtitle=element_text(size=11, color=INK_SOFT),\n        axis_title_x=element_text(size=12, color=INK),\n        axis_title_y=element_blank(),\n        axis_text_x=element_text(size=10, color=INK_SOFT),\n        axis_text_y=element_text(size=10, face=\"bold\", color=INK_SOFT),\n        legend_position=\"bottom\",\n        legend_direction=\"horizontal\",\n        legend_title=element_text(size=10, face=\"bold\", color=INK),\n        legend_text=element_text(size=10, color=INK_SOFT),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        panel_grid_major_y=element_blank(),\n        panel_grid_minor=element_blank(),\n        panel_grid_major_x=element_line(size=0.3, color=INK_SOFT),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n    )\n    + ggsize(800, 450)\n)\n\n# Save — theme-suffixed, scale=4 yields 3200×1800 px\nggsave(plot, f\"plot-{THEME}.png\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\")\n\n# Move from lets-plot-images subfolder if ggsave placed files there\nfor fname in [f\"plot-{THEME}.png\", f\"plot-{THEME}.html\"]:\n    src = os.path.join(\"lets-plot-images\", fname)\n    if os.path.exists(src):\n        shutil.move(src, fname)\nif os.path.exists(\"lets-plot-images\"):\n    shutil.rmtree(\"lets-plot-images\")\n"}