{"spec_id":"bullet-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// bullet-basic: Basic Bullet Chart\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 84/100 | Created: 2026-08-24\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Regional sales-quota attainment: actual % vs. target %, all bullets share\n// the same poor/satisfactory/good scale so they compare directly.\nconst ranges = [50, 75, 100]; // poor | satisfactory | good thresholds (%)\nconst regions = [\n  { label: \"North America\", actual: 82, target: 90 },\n  { label: \"EMEA\", actual: 68, target: 75 },\n  { label: \"APAC\", actual: 91, target: 85 },\n  { label: \"LATAM\", actual: 55, target: 70 },\n  { label: \"ANZ\", actual: 74, target: 80 },\n];\n\n// --- SVG mount ----------------------------------------------------------------\nconst margin = { top: 112, right: 60, bottom: 60, left: 190 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scale --------------------------------------------------------------------\nconst x = d3.scaleLinear().domain([0, ranges[ranges.length - 1]]).range([0, iw]);\n\n// --- Rows -----------------------------------------------------------------------\nconst rowHeight = ih / regions.length;\nconst bandHeight = rowHeight * 0.56;\nconst barHeight = rowHeight * 0.26;\nconst bandOpacity = [0.22, 0.13, 0.11]; // poor -> good, still legible on the darkest theme\n\n// The region furthest below target gets a callout treatment.\nconst worstLabel = regions.reduce((a, b) => (a.actual - a.target < b.actual - b.target ? a : b)).label;\n\nconst row = g\n  .selectAll(\".row\")\n  .data(regions)\n  .join(\"g\")\n  .attr(\"class\", \"row\")\n  .attr(\"transform\", (d, i) => `translate(0,${i * rowHeight + rowHeight / 2})`);\n\n// Qualitative range bands: nested data-join, one segment per range edge.\nrow.each(function (d) {\n  let prev = 0;\n  const segments = ranges.map((edge, i) => {\n    const seg = { x0: prev, x1: edge, opacity: bandOpacity[i] };\n    prev = edge;\n    return seg;\n  });\n  d3.select(this)\n    .selectAll(\"rect.band\")\n    .data(segments)\n    .join(\"rect\")\n    .attr(\"class\", \"band\")\n    .attr(\"x\", (s) => x(s.x0))\n    .attr(\"y\", -bandHeight / 2)\n    .attr(\"width\", (s) => x(s.x1) - x(s.x0))\n    .attr(\"height\", bandHeight)\n    .attr(\"fill\", t.ink)\n    .attr(\"fill-opacity\", (s) => s.opacity);\n});\n\n// Actual measure bar\nrow\n  .append(\"rect\")\n  .attr(\"x\", 0)\n  .attr(\"y\", -barHeight / 2)\n  .attr(\"width\", (d) => x(d.actual))\n  .attr(\"height\", barHeight)\n  .attr(\"fill\", t.palette[0])\n  .append(\"title\")\n  .text((d) => `${d.label}: ${d.actual}% actual`);\n\n// Target marker: vertical stem capped with diamonds, a shape distinct from the bar/bands.\nconst markerHalf = bandHeight / 2 + 10;\nconst diamond = 7;\nrow\n  .append(\"line\")\n  .attr(\"x1\", (d) => x(d.target))\n  .attr(\"x2\", (d) => x(d.target))\n  .attr(\"y1\", -markerHalf)\n  .attr(\"y2\", markerHalf)\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 3);\nrow\n  .append(\"path\")\n  .attr(\n    \"d\",\n    (d) =>\n      `M ${x(d.target)} ${-markerHalf - diamond} L ${x(d.target) + diamond} ${-markerHalf} L ${x(d.target)} ${-markerHalf + diamond} L ${x(d.target) - diamond} ${-markerHalf} Z`,\n  )\n  .attr(\"fill\", t.ink);\nrow\n  .append(\"path\")\n  .attr(\n    \"d\",\n    (d) =>\n      `M ${x(d.target)} ${markerHalf - diamond} L ${x(d.target) + diamond} ${markerHalf} L ${x(d.target)} ${markerHalf + diamond} L ${x(d.target) - diamond} ${markerHalf} Z`,\n  )\n  .attr(\"fill\", t.ink)\n  .append(\"title\")\n  .text((d) => `${d.label}: ${d.target}% target`);\n\n// Row label — the region furthest below target gets a callout treatment.\nrow\n  .append(\"text\")\n  .attr(\"x\", -16)\n  .attr(\"y\", 0)\n  .attr(\"dy\", \"0.35em\")\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", (d) => (d.label === worstLabel ? t.amber : t.ink))\n  .style(\"font-size\", \"16px\")\n  .style(\"font-weight\", (d) => (d.label === worstLabel ? \"700\" : \"500\"))\n  .text((d) => (d.label === worstLabel ? `${d.label} ▾` : d.label));\n\n// Actual-value readout\nrow\n  .append(\"text\")\n  .attr(\"x\", iw + 14)\n  .attr(\"y\", 0)\n  .attr(\"dy\", \"0.35em\")\n  .attr(\"text-anchor\", \"start\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-variant-numeric\", \"tabular-nums\")\n  .text((d) => `${d.actual}%`);\n\n// --- Axis -----------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).tickValues([0, 25, 50, 75, 100]).tickFormat((v) => `${v}%`));\nxAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\");\nxAxis.selectAll(\"line\").attr(\"stroke\", t.grid);\nxAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 46)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"Quota Attainment\");\n\n// --- Legend ---------------------------------------------------------------------\nconst legendItems = [\n  { type: \"band\", color: t.ink, opacity: bandOpacity[0], text: \"Poor\" },\n  { type: \"band\", color: t.ink, opacity: bandOpacity[1], text: \"Satisfactory\" },\n  { type: \"band\", color: t.ink, opacity: bandOpacity[2], text: \"Good\" },\n  { type: \"bar\", color: t.palette[0], text: \"Actual\" },\n  { type: \"line\", color: t.ink, text: \"Target\" },\n];\n\nconst legend = svg.append(\"g\").attr(\"transform\", `translate(0,56)`);\nlet lx = 0;\nfor (const item of legendItems) {\n  const li = legend.append(\"g\").attr(\"transform\", `translate(${lx},0)`);\n  if (item.type === \"line\") {\n    li.append(\"line\").attr(\"x1\", 8).attr(\"x2\", 8).attr(\"y1\", -7).attr(\"y2\", 7).attr(\"stroke\", item.color).attr(\"stroke-width\", 3);\n    li.append(\"path\").attr(\"d\", \"M 8 -10 L 12 -7 L 8 -4 L 4 -7 Z\").attr(\"fill\", item.color);\n  } else {\n    li.append(\"rect\")\n      .attr(\"x\", 0)\n      .attr(\"y\", -7)\n      .attr(\"width\", 16)\n      .attr(\"height\", 14)\n      .attr(\"fill\", item.color)\n      .attr(\"fill-opacity\", item.opacity ?? 1);\n  }\n  const label = li\n    .append(\"text\")\n    .attr(\"x\", 24)\n    .attr(\"y\", 0)\n    .attr(\"dy\", \"0.35em\")\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"13px\")\n    .text(item.text);\n  lx += 24 + label.node().getComputedTextLength() + 26;\n}\nconst legendWidth = lx - 26;\nlegend.attr(\"transform\", `translate(${(width - legendWidth) / 2},56)`);\n\n// --- Title ------------------------------------------------------------------------\nconst title = \"Regional Sales Quota Attainment · bullet-basic · javascript · d3 · anyplot.ai\";\nconst titleFontSize = Math.max(15, Math.round(22 * Math.min(1, 67 / title.length)));\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 32)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", `${titleFontSize}px`)\n  .style(\"font-weight\", \"600\")\n  .text(title);\n"}