{"spec_id":"point-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// point-basic: Point Estimate Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 90, right: 90, bottom: 80, left: 220 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: customer satisfaction survey, mean score (1-10) with 95% CI -----\nconst data = [\n  { category: \"Checkout Flow\", estimate: 8.4, lower: 8.0, upper: 8.8 },\n  { category: \"Search Relevance\", estimate: 7.6, lower: 7.1, upper: 8.1 },\n  { category: \"Delivery Speed\", estimate: 6.9, lower: 6.3, upper: 7.5 },\n  { category: \"Mobile App\", estimate: 7.2, lower: 6.8, upper: 7.6 },\n  { category: \"Customer Support\", estimate: 8.1, lower: 7.6, upper: 8.6 },\n  { category: \"Product Packaging\", estimate: 6.3, lower: 5.6, upper: 7.0 },\n  { category: \"Return Process\", estimate: 5.8, lower: 5.1, upper: 6.5 },\n];\n\n// sort descending by estimate so the eye reads a clean ranking top to bottom\ndata.sort((a, b) => d3.descending(a.estimate, b.estimate));\nconst meanEstimate = d3.mean(data, (d) => d.estimate);\nconst maxUpper = d3.max(data, (d) => d.upper);\nconst fmt = d3.format(\".1f\");\n\n// --- SVG mount ---------------------------------------------------------------\nconst svg = d3\n  .select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\nconst g = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scales --------------------------------------------------------------\nconst x = d3.scaleLinear().domain([0, 10]).nice().range([0, iw]);\nconst y = d3\n  .scaleBand()\n  .domain(data.map((d) => d.category))\n  .range([0, ih])\n  .padding(0.4);\n\n// --- Gridlines (x-axis only, subtle) ---------------------------------------\ng.append(\"g\")\n  .attr(\"class\", \"grid\")\n  .selectAll(\"line\")\n  .data(x.ticks(6))\n  .join(\"line\")\n  .attr(\"x1\", (d) => x(d))\n  .attr(\"x2\", (d) => x(d))\n  .attr(\"y1\", 0)\n  .attr(\"y2\", ih)\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// --- Axes ------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(\n    d3\n      .axisBottom(x)\n      .ticks(6)\n      .tickSize(0)\n      .tickPadding(14)\n      .tickFormat(d3.format(\".0f\")),\n  );\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).tickSize(0).tickPadding(16));\n\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"16px\");\n}\nxAxis.select(\".domain\").remove();\nyAxis.select(\".domain\").remove();\n\n// --- Reference line: overall mean, the focal point the ranking is read against ---\n// Sized/opacity-boosted (vs. a thin 1px hairline) so the dashes survive\n// downsampling and stay unmistakable next to the bolder whiskers/caps.\ng.append(\"line\")\n  .attr(\"x1\", x(meanEstimate))\n  .attr(\"x2\", x(meanEstimate))\n  .attr(\"y1\", 0)\n  .attr(\"y2\", ih)\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"stroke-dasharray\", \"10,6\")\n  .attr(\"opacity\", 0.65);\n\ng.append(\"text\")\n  .attr(\"x\", x(meanEstimate))\n  .attr(\"y\", -18)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .style(\"font-weight\", \"600\")\n  .text(`Mean: ${fmt(meanEstimate)}`);\n\n// --- Axis labels -------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Mean Satisfaction Score (1–10 scale)\");\n\n// --- Confidence interval whiskers + caps ------------------------------------\nconst rows = g\n  .selectAll(\".row\")\n  .data(data)\n  .join(\"g\")\n  .attr(\"class\", \"row\")\n  .attr(\n    \"transform\",\n    (d) => `translate(0,${y(d.category) + y.bandwidth() / 2})`,\n  );\n\nconst capHalf = 9;\n\n// whiskers + caps rendered at reduced opacity so the saturated point marker\n// reads as the primary focal point of each row\nrows\n  .append(\"line\")\n  .attr(\"x1\", (d) => x(d.lower))\n  .attr(\"x2\", (d) => x(d.upper))\n  .attr(\"y1\", 0)\n  .attr(\"y2\", 0)\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"opacity\", 0.45);\n\nrows\n  .append(\"line\")\n  .attr(\"x1\", (d) => x(d.lower))\n  .attr(\"x2\", (d) => x(d.lower))\n  .attr(\"y1\", -capHalf)\n  .attr(\"y2\", capHalf)\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"opacity\", 0.7);\n\nrows\n  .append(\"line\")\n  .attr(\"x1\", (d) => x(d.upper))\n  .attr(\"x2\", (d) => x(d.upper))\n  .attr(\"y1\", -capHalf)\n  .attr(\"y2\", capHalf)\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"opacity\", 0.7);\n\n// --- Point estimates ---------------------------------------------------------\nrows\n  .append(\"circle\")\n  .attr(\"cx\", (d) => x(d.estimate))\n  .attr(\"cy\", 0)\n  .attr(\"r\", 10)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2);\n\n// value labels via d3.format, aligned in a single column past the widest\n// whisker so they never collide with a cap or the mean reference line\nrows\n  .append(\"text\")\n  .attr(\"x\", x(maxUpper) + 24)\n  .attr(\"y\", 5)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .style(\"font-weight\", \"600\")\n  .text((d) => fmt(d.estimate));\n\n// --- Title -------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 48)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"26px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"point-basic · javascript · d3 · anyplot.ai\");\n"}