{"spec_id":"errorbar-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// errorbar-basic: Basic Error Bar Plot\n// Library: d3 7.9.0 | JavaScript 22.23.1\n// Quality: 86/100 | Created: 2026-06-30\n\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\nconst margin = { top: 85, right: 55, bottom: 95, left: 105 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// Crop yield (t/ha) by fertilizer treatment — field trial, n=20 plots per group\nconst data = [\n  { treatment: \"Control\",    mean: 3.2, sd: 0.38 },\n  { treatment: \"Nitrogen\",   mean: 4.8, sd: 0.52 },\n  { treatment: \"Phosphorus\", mean: 4.1, sd: 0.45 },\n  { treatment: \"Potassium\",  mean: 3.9, sd: 0.41 },\n  { treatment: \"NPK\",        mean: 5.7, sd: 0.61 },\n  { treatment: \"Organic\",    mean: 4.4, sd: 0.49 },\n];\n\n// SVG mount\nconst svg = d3.select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// Ordinal color scale — each treatment gets a distinct Imprint palette color\nconst color = d3.scaleOrdinal()\n  .domain(data.map((d) => d.treatment))\n  .range(t.palette);\n\n// Scales\nconst x = d3.scaleBand()\n  .domain(data.map((d) => d.treatment))\n  .range([0, iw])\n  .padding(0.35);\n\nconst yMin = d3.min(data, (d) => d.mean - d.sd) - 0.5;\nconst y = d3.scaleLinear()\n  .domain([yMin, d3.max(data, (d) => d.mean + d.sd) + 0.4])\n  .nice()\n  .range([ih, 0]);\n\n// Helper: horizontal center of each band\nconst cx = (d) => x(d.treatment) + x.bandwidth() / 2;\nconst capHalf = x.bandwidth() * 0.22;\n\n// Subtle column highlight behind the top-performing NPK treatment\nconst npk = data.find((d) => d.treatment === \"NPK\");\ng.append(\"rect\")\n  .attr(\"x\", x(\"NPK\") - 8)\n  .attr(\"y\", 0)\n  .attr(\"width\", x.bandwidth() + 16)\n  .attr(\"height\", ih)\n  .attr(\"fill\", color(\"NPK\"))\n  .attr(\"opacity\", 0.07)\n  .attr(\"rx\", 4);\n\n// Horizontal gridlines (y-axis only)\nconst gridG = g.append(\"g\").attr(\"class\", \"grid\");\ngridG.call(d3.axisLeft(y).tickSize(-iw).tickFormat(\"\").ticks(7));\ngridG.selectAll(\"line\").attr(\"stroke\", t.grid).attr(\"stroke-width\", 1);\ngridG.select(\".domain\").remove();\n\n// Axes\nconst xAxis = g.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).tickSizeOuter(0));\n\nconst yAxis = g.append(\"g\")\n  .call(d3.axisLeft(y).ticks(7).tickFormat((d) => d.toFixed(1)).tickSizeOuter(0));\n\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.grid);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// Vertical error bar stems\ng.selectAll(\".errbar\").data(data).join(\"line\")\n  .attr(\"class\", \"errbar\")\n  .attr(\"x1\", cx).attr(\"x2\", cx)\n  .attr(\"y1\", (d) => y(d.mean - d.sd))\n  .attr(\"y2\", (d) => y(d.mean + d.sd))\n  .attr(\"stroke\", (d) => color(d.treatment))\n  .attr(\"stroke-width\", 2.5);\n\n// Upper caps\ng.selectAll(\".cap-top\").data(data).join(\"line\")\n  .attr(\"class\", \"cap-top\")\n  .attr(\"x1\", (d) => cx(d) - capHalf).attr(\"x2\", (d) => cx(d) + capHalf)\n  .attr(\"y1\", (d) => y(d.mean + d.sd)).attr(\"y2\", (d) => y(d.mean + d.sd))\n  .attr(\"stroke\", (d) => color(d.treatment)).attr(\"stroke-width\", 2.5);\n\n// Lower caps\ng.selectAll(\".cap-bot\").data(data).join(\"line\")\n  .attr(\"class\", \"cap-bot\")\n  .attr(\"x1\", (d) => cx(d) - capHalf).attr(\"x2\", (d) => cx(d) + capHalf)\n  .attr(\"y1\", (d) => y(d.mean - d.sd)).attr(\"y2\", (d) => y(d.mean - d.sd))\n  .attr(\"stroke\", (d) => color(d.treatment)).attr(\"stroke-width\", 2.5);\n\n// Mean value circles\ng.selectAll(\"circle\").data(data).join(\"circle\")\n  .attr(\"cx\", cx)\n  .attr(\"cy\", (d) => y(d.mean))\n  .attr(\"r\", 9)\n  .attr(\"fill\", (d) => color(d.treatment))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2.5);\n\n// Mean value labels — shown below each circle for quick reading\ng.selectAll(\".mean-val\").data(data).join(\"text\")\n  .attr(\"class\", \"mean-val\")\n  .attr(\"x\", cx)\n  .attr(\"y\", (d) => y(d.mean) + 26)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", (d) => color(d.treatment))\n  .style(\"font-size\", \"13px\")\n  .style(\"font-weight\", \"500\")\n  .text((d) => d.mean.toFixed(1));\n\n// NPK best-performer annotation\ng.append(\"text\")\n  .attr(\"x\", cx(npk))\n  .attr(\"y\", y(npk.mean + npk.sd) - 16)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", color(\"NPK\"))\n  .style(\"font-size\", \"13px\")\n  .style(\"font-weight\", \"700\")\n  .text(\"★ Top yield\");\n\n// Axis labels\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 68)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Fertilizer Treatment\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -75)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Crop Yield (t/ha)\");\n\n// Title\nsvg.append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 52)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"errorbar-basic · javascript · d3 · anyplot.ai\");\n"}