{"spec_id":"bullet-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// bullet-basic: Basic Bullet Chart\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-08-24\n\nconst t = window.ANYPLOT_TOKENS;\nconst theme = window.ANYPLOT_THEME || \"light\";\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Quarterly KPIs normalized to a common \"% of target\" scale so the bullets\n// stay comparable at a glance, per anyplot's own dashboard-alignment note.\n// Market Share deliberately lands in the \"Poor\" band so the demo covers all\n// three qualitative outcomes, not just satisfactory/good.\nconst metrics = [\n  { name: \"Revenue Growth\", actual: 92, target: 100 },\n  { name: \"Customer Retention\", actual: 105, target: 100 },\n  { name: \"Market Share\", actual: 45, target: 100 },\n  { name: \"Net Promoter Score\", actual: 130, target: 100 },\n];\nconst ranges = [60, 85, 140]; // poor / satisfactory / good band boundaries\nconst rangeBounds = [\n  [0, ranges[0]],\n  [ranges[0], ranges[1]],\n  [ranges[1], ranges[2]],\n];\n\n// Grayscale bands read as chrome (structural, not data) — built from the ink\n// token so they stay theme-adaptive while staying strictly grayscale.\nconst inkRGB = theme === \"light\" ? \"26,26,23\" : \"240,239,232\";\nconst bandColors = [`rgba(${inkRGB},0.10)`, `rgba(${inkRGB},0.18)`, `rgba(${inkRGB},0.28)`];\n\nconst categories = metrics.map((m) => m.name);\nconst bandThickness = 70;\nconst actualThickness = 26;\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Custom draw: qualitative bands + target ticks + value labels -----------\n// Core Chart.js plugin API (beforeDatasetsDraw / afterDatasetsDraw hooks) — no\n// external plugin package. Chart.js's `grouped: false` does not actually\n// overlay multiple bar datasets on the same row in this version (each dataset\n// still gets its own lane), so bands, target ticks, and labels are all drawn\n// manually here off the single \"Actual\" bar's own row geometry — guaranteeing\n// every visual element for a row shares one y-center and never drifts apart.\nconst bulletExtrasPlugin = {\n  id: \"bulletExtras\",\n  beforeDatasetsDraw(chart) {\n    const { ctx, scales } = chart;\n    const meta = chart.getDatasetMeta(0);\n    const half = bandThickness / 2;\n    ctx.save();\n    metrics.forEach((m, i) => {\n      const row = meta.data[i];\n      rangeBounds.forEach(([lo, hi], bandIdx) => {\n        const x0 = scales.x.getPixelForValue(lo);\n        const x1 = scales.x.getPixelForValue(hi);\n        ctx.fillStyle = bandColors[bandIdx];\n        ctx.fillRect(x0, row.y - half, x1 - x0, bandThickness);\n      });\n    });\n    ctx.restore();\n  },\n  afterDatasetsDraw(chart) {\n    const { ctx, scales } = chart;\n    const meta = chart.getDatasetMeta(0);\n    const half = (bandThickness / 2) * 1.35;\n    ctx.save();\n    metrics.forEach((m, i) => {\n      const row = meta.data[i];\n      const targetX = scales.x.getPixelForValue(m.target);\n      ctx.strokeStyle = t.ink;\n      ctx.lineWidth = 4;\n      ctx.beginPath();\n      ctx.moveTo(targetX, row.y - half);\n      ctx.lineTo(targetX, row.y + half);\n      ctx.stroke();\n\n      // Shape-coded (not hue-coded) over/under-target cue — keeps the CVD-safe\n      // all-ink text while still calling out which metrics missed target.\n      const arrow = m.actual >= m.target ? \"▲\" : \"▼\";\n      ctx.fillStyle = t.ink;\n      ctx.font = \"600 16px sans-serif\";\n      ctx.textBaseline = \"middle\";\n      ctx.textAlign = \"left\";\n      ctx.fillText(`${arrow} ${m.actual}%`, row.x + 10, row.y);\n    });\n    ctx.restore();\n  },\n};\n\n// --- Chart ---------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"bar\",\n  data: {\n    labels: categories,\n    datasets: [\n      {\n        label: \"Actual\",\n        data: metrics.map((m) => [0, m.actual]),\n        backgroundColor: t.palette[0],\n        barThickness: actualThickness,\n        borderWidth: 0,\n      },\n    ],\n  },\n  options: {\n    indexAxis: \"y\",\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { right: 60 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"Quarterly KPI Dashboard · bullet-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      legend: {\n        position: \"bottom\",\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          generateLabels: () => [\n            { text: \"Poor\", fillStyle: bandColors[0], strokeStyle: bandColors[0], lineWidth: 0 },\n            { text: \"Satisfactory\", fillStyle: bandColors[1], strokeStyle: bandColors[1], lineWidth: 0 },\n            { text: \"Good\", fillStyle: bandColors[2], strokeStyle: bandColors[2], lineWidth: 0 },\n            { text: \"Actual\", fillStyle: t.palette[0], strokeStyle: t.palette[0], lineWidth: 0 },\n            { text: \"Target\", fillStyle: t.ink, strokeStyle: t.ink, lineWidth: 0 },\n          ],\n        },\n      },\n    },\n    scales: {\n      x: {\n        min: 0,\n        max: 150,\n        ticks: { color: t.inkSoft, font: { size: 14 }, callback: (v) => `${v}%` },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Percent of Quarterly Target\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { display: false },\n      },\n    },\n  },\n  plugins: [bulletExtrasPlugin],\n});\n"}