{"spec_id":"burndown-sprint","library":"d3","language":"javascript","code":"// anyplot.ai\n// burndown-sprint: Agile Sprint Burndown Chart\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 92/100 | Created: 2026-06-14\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 98, right: 165, bottom: 85, left: 92 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: 10-working-day sprint Jun 1–12 2026 (weekends Jun 6–7) ---\n// Initial scope: 40 story points; scope change +8 on Jun 4 (burned 5, added 8 → net +3)\nconst parseDate = d3.timeParse(\"%Y-%m-%d\");\n\nconst actualData = [\n  { date: parseDate(\"2026-06-01\"), value: 40 },\n  { date: parseDate(\"2026-06-02\"), value: 34 },\n  { date: parseDate(\"2026-06-03\"), value: 28 },\n  { date: parseDate(\"2026-06-04\"), value: 31 },\n  { date: parseDate(\"2026-06-05\"), value: 25 },\n  { date: parseDate(\"2026-06-08\"), value: 17 },\n  { date: parseDate(\"2026-06-09\"), value: 11 },\n  { date: parseDate(\"2026-06-10\"), value: 6 },\n  { date: parseDate(\"2026-06-11\"), value: 2 },\n  { date: parseDate(\"2026-06-12\"), value: 0 },\n];\n\n// Ideal line: straight from (Jun 1, 40) to (Jun 12, 0)\nconst idealData = [\n  { date: parseDate(\"2026-06-01\"), value: 40 },\n  { date: parseDate(\"2026-06-12\"), value: 0 },\n];\n\nconst scopeChangeDate = parseDate(\"2026-06-04\");\nconst weekendStart = parseDate(\"2026-06-06\");\nconst weekendEnd = parseDate(\"2026-06-08\");\n\n// --- SVG mount ---\nconst svg = d3.select(\"#container\").append(\"svg\")\n  .attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scales ---\nconst x = d3.scaleTime()\n  .domain([parseDate(\"2026-06-01\"), parseDate(\"2026-06-12\")])\n  .range([0, iw]);\nconst y = d3.scaleLinear()\n  .domain([0, 48])\n  .range([ih, 0]);\n\n// --- Weekend band ---\nconst isLight = window.ANYPLOT_THEME === \"light\";\ng.append(\"rect\")\n  .attr(\"x\", x(weekendStart))\n  .attr(\"y\", 0)\n  .attr(\"width\", x(weekendEnd) - x(weekendStart))\n  .attr(\"height\", ih)\n  .attr(\"fill\", isLight ? \"rgba(0,0,0,0.045)\" : \"rgba(255,255,255,0.07)\");\n\ng.append(\"text\")\n  .attr(\"x\", (x(weekendStart) + x(weekendEnd)) / 2)\n  .attr(\"y\", y(8))\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text(\"Weekend\");\n\n// --- Horizontal gridlines ---\ng.append(\"g\")\n  .call(d3.axisLeft(y).tickSize(-iw).tickFormat(\"\").ticks(6))\n  .call(ax => ax.select(\".domain\").remove())\n  .call(ax => ax.selectAll(\"line\")\n    .attr(\"stroke\", t.grid)\n    .attr(\"stroke-width\", 1));\n\n// --- Ideal burndown line (dashed, muted) ---\ng.append(\"path\")\n  .datum(idealData)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"stroke-dasharray\", \"10,7\")\n  .attr(\"d\", d3.line().x(d => x(d.date)).y(d => y(d.value)));\n\n// --- Actual remaining (step-after curve, brand green) ---\ng.append(\"path\")\n  .datum(actualData)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 3.5)\n  .attr(\"d\", d3.line()\n    .x(d => x(d.date))\n    .y(d => y(d.value))\n    .curve(d3.curveStepAfter));\n\n// Data point markers\ng.selectAll(\".dot\")\n  .data(actualData)\n  .join(\"circle\")\n  .attr(\"class\", \"dot\")\n  .attr(\"cx\", d => x(d.date))\n  .attr(\"cy\", d => y(d.value))\n  .attr(\"r\", 5)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2.5);\n\n// --- Scope change marker (matte red vertical dashed line + D3 triangle tip) ---\ng.append(\"line\")\n  .attr(\"x1\", x(scopeChangeDate)).attr(\"x2\", x(scopeChangeDate))\n  .attr(\"y1\", 0).attr(\"y2\", ih)\n  .attr(\"stroke\", t.palette[4])\n  .attr(\"stroke-width\", 2)\n  .attr(\"stroke-dasharray\", \"6,4\");\n\n// Triangle arrowhead at the top of the scope-change line (pointing down = event occurred here)\ng.append(\"path\")\n  .attr(\"d\", d3.symbol().type(d3.symbolTriangle).size(80)())\n  .attr(\"transform\", `translate(${x(scopeChangeDate)}, 8) rotate(180)`)\n  .attr(\"fill\", t.palette[4]);\n\n// Scope change annotation\nconst scx = x(scopeChangeDate) + 8;\ng.append(\"text\")\n  .attr(\"x\", scx).attr(\"y\", y(44))\n  .attr(\"fill\", t.palette[4])\n  .style(\"font-size\", \"13px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"+8 pts added\");\n\n// --- X axis ---\nconst xAxis = g.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(\n    d3.axisBottom(x)\n      .tickValues(actualData.map(d => d.date))\n      .tickFormat(d => `${d3.timeFormat(\"%b\")(d)} ${d.getDate()}`)\n  );\nxAxis.selectAll(\"text\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .attr(\"transform\", \"rotate(-35)\")\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"dx\", \"-0.4em\")\n  .attr(\"dy\", \"0.2em\");\nxAxis.selectAll(\"line\").attr(\"stroke\", t.grid);\nxAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// --- Y axis ---\nconst yAxis = g.append(\"g\")\n  .call(d3.axisLeft(y).ticks(6));\nyAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nyAxis.selectAll(\"line\").attr(\"stroke\", t.grid);\nyAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// --- Axis labels ---\nsvg.append(\"text\")\n  .attr(\"x\", margin.left + iw / 2)\n  .attr(\"y\", height - 10)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"15px\")\n  .text(\"Sprint Day\");\n\nsvg.append(\"text\")\n  .attr(\"transform\", `translate(20,${margin.top + ih / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"15px\")\n  .text(\"Remaining Story Points\");\n\n// --- Legend (top-right of plot area) ---\nconst lx = iw - 210;\nconst ly = 8;\n\n// Elevated-bg legend frame\ng.append(\"rect\")\n  .attr(\"x\", lx - 10).attr(\"y\", ly - 10)\n  .attr(\"width\", 220).attr(\"height\", 92)\n  .attr(\"rx\", 4)\n  .attr(\"fill\", t.elevatedBg)\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\ng.append(\"line\")\n  .attr(\"x1\", lx).attr(\"y1\", ly + 8).attr(\"x2\", lx + 40).attr(\"y2\", ly + 8)\n  .attr(\"stroke\", t.palette[0]).attr(\"stroke-width\", 3.5);\ng.append(\"circle\")\n  .attr(\"cx\", lx + 20).attr(\"cy\", ly + 8).attr(\"r\", 4)\n  .attr(\"fill\", t.palette[0]).attr(\"stroke\", t.pageBg).attr(\"stroke-width\", 2);\ng.append(\"text\")\n  .attr(\"x\", lx + 48).attr(\"y\", ly + 13)\n  .attr(\"fill\", t.ink).style(\"font-size\", \"14px\").text(\"Actual remaining\");\n\ng.append(\"line\")\n  .attr(\"x1\", lx).attr(\"y1\", ly + 36).attr(\"x2\", lx + 40).attr(\"y2\", ly + 36)\n  .attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 2.5).attr(\"stroke-dasharray\", \"10,7\");\ng.append(\"text\")\n  .attr(\"x\", lx + 48).attr(\"y\", ly + 41)\n  .attr(\"fill\", t.ink).style(\"font-size\", \"14px\").text(\"Ideal burndown\");\n\ng.append(\"line\")\n  .attr(\"x1\", lx).attr(\"y1\", ly + 63).attr(\"x2\", lx + 40).attr(\"y2\", ly + 63)\n  .attr(\"stroke\", t.palette[4]).attr(\"stroke-width\", 2).attr(\"stroke-dasharray\", \"6,4\");\ng.append(\"text\")\n  .attr(\"x\", lx + 48).attr(\"y\", ly + 68)\n  .attr(\"fill\", t.ink).style(\"font-size\", \"14px\").text(\"Scope change\");\n\n// --- Title and subtitle ---\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 48)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"burndown-sprint · javascript · d3 · anyplot.ai\");\n\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 72)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"Below ideal = ahead of schedule · 10-day sprint, Jun 1–12 2026\");\n"}