{"spec_id":"burndown-sprint","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// burndown-sprint: Agile Sprint Burndown Chart\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 92/100 | Created: 2026-06-14\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst THEME = window.ANYPLOT_THEME;\n\n// Sprint: 14 calendar days (Jul 7–20), 10 working days, 40 initial story points.\n// Scope change on Jul 10 (Day 4): +9 story points added mid-sprint.\nconst labels = [\n  \"Jul 7\", \"Jul 8\", \"Jul 9\", \"Jul 10\", \"Jul 11\", \"Jul 12\", \"Jul 13\",\n  \"Jul 14\", \"Jul 15\", \"Jul 16\", \"Jul 17\", \"Jul 18\", \"Jul 19\", \"Jul 20\",\n];\n\n// Actual remaining story points at end of each day (step-wise; flat on weekends).\n// Day 4 (Jul 10) jumps from 29→37: scope grew by 9 pts, 1 pt was burned that day.\nconst remaining = [40, 35, 29, 37, 31, 31, 31, 25, 18, 12, 5, 0, 0, 0];\n\n// Ideal burndown: straight diagonal from 40 (day 0) to 0 (day 13).\nconst ideal = labels.map((_, i) => +(40 * (1 - i / 13)).toFixed(1));\n\n// Weekend bands: Jul 12–13 and Jul 19–20 (half-unit offsets centre on the gaps).\nconst weekendRanges = [[4.5, 6.5], [11.5, 13.5]];\nconst scopeChangeIndex = 3; // Jul 10\n\n// Plugin: shade non-working weekend columns behind the chart data.\nconst weekendPlugin = {\n  id: \"weekendBands\",\n  beforeDraw(chart) {\n    const { ctx, scales: { x, y } } = chart;\n    ctx.save();\n    ctx.fillStyle = THEME === \"light\"\n      ? \"rgba(26,26,23,0.055)\"\n      : \"rgba(240,239,232,0.055)\";\n    weekendRanges.forEach(([s, e]) => {\n      const x0 = x.getPixelForValue(s);\n      const x1 = Math.min(x.getPixelForValue(e), x.right);\n      ctx.fillRect(x0, y.top, x1 - x0, y.bottom - y.top);\n    });\n    // Weekend label in first band\n    const midX = (x.getPixelForValue(4.5) + x.getPixelForValue(6.5)) / 2;\n    ctx.fillStyle = THEME === \"light\"\n      ? \"rgba(26,26,23,0.25)\"\n      : \"rgba(240,239,232,0.25)\";\n    ctx.font = `13px ${Chart.defaults.font.family}`;\n    ctx.textAlign = \"center\";\n    ctx.fillText(\"Weekend\", midX, y.bottom - 14);\n    ctx.restore();\n  },\n};\n\n// Plugin: vertical amber line + label at the scope change day.\nconst scopeChangePlugin = {\n  id: \"scopeChangeMarker\",\n  afterDatasetsDraw(chart) {\n    const { ctx, scales: { x, y } } = chart;\n    const xPos = x.getPixelForValue(scopeChangeIndex);\n    ctx.save();\n    ctx.setLineDash([5, 4]);\n    ctx.strokeStyle = t.amber;\n    ctx.lineWidth = 2.5;\n    ctx.beginPath();\n    ctx.moveTo(xPos, y.top);\n    ctx.lineTo(xPos, y.bottom);\n    ctx.stroke();\n    ctx.setLineDash([]);\n    ctx.fillStyle = t.amber;\n    ctx.font = `bold 14px ${Chart.defaults.font.family}`;\n    ctx.textAlign = \"left\";\n    ctx.fillText(\"+9 pts\", xPos + 7, y.top + 26);\n    ctx.font = `13px ${Chart.defaults.font.family}`;\n    ctx.fillText(\"scope added\", xPos + 7, y.top + 44);\n    ctx.restore();\n  },\n};\n\n// Mount canvas into the harness container.\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\nnew Chart(canvas, {\n  type: \"line\",\n  data: {\n    labels,\n    datasets: [\n      {\n        label: \"Actual Remaining\",\n        data: remaining,\n        borderColor: t.palette[0],     // Imprint brand green\n        borderWidth: 3,\n        pointRadius: 5,\n        pointHoverRadius: 7,\n        pointBackgroundColor: t.palette[0],\n        pointBorderColor: t.pageBg,\n        pointBorderWidth: 2,\n        stepped: \"after\",\n        // Red fill when actual > ideal (behind schedule); green when actual < ideal (ahead).\n        // Dark mode uses higher opacity so fills remain visible on the near-black surface.\n        fill: {\n          target: 1,\n          above: THEME === \"dark\" ? \"rgba(174,48,48,0.22)\" : \"rgba(174,48,48,0.12)\",\n          below: THEME === \"dark\" ? \"rgba(0,158,115,0.22)\" : \"rgba(0,158,115,0.12)\",\n        },\n      },\n      {\n        label: \"Ideal Burndown\",\n        data: ideal,\n        borderColor: t.inkSoft,\n        borderWidth: 2,\n        borderDash: [8, 5],\n        pointRadius: 0,\n        tension: 0,\n        fill: false,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    interaction: {\n      mode: \"index\",\n      intersect: false,\n    },\n    plugins: {\n      title: {\n        display: true,\n        text: \"burndown-sprint · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"bold\" },\n        padding: { top: 8, bottom: 16 },\n      },\n      legend: {\n        position: \"top\",\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          padding: 24,\n          usePointStyle: true,\n          pointStyleWidth: 20,\n        },\n      },\n    },\n    scales: {\n      x: {\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          maxRotation: 45,\n          minRotation: 0,\n        },\n        grid: { color: t.grid },\n        border: { display: false },\n        title: {\n          display: true,\n          text: \"Sprint Day\",\n          color: t.ink,\n          font: { size: 16 },\n          padding: { top: 8 },\n        },\n      },\n      y: {\n        beginAtZero: true,\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n        },\n        grid: { color: t.grid },\n        border: { display: false },\n        title: {\n          display: true,\n          text: \"Remaining Story Points\",\n          color: t.ink,\n          font: { size: 16 },\n          padding: { bottom: 8 },\n        },\n      },\n    },\n  },\n  plugins: [weekendPlugin, scopeChangePlugin],\n});\n"}