{"spec_id":"line-stepwise","library":"d3","language":"javascript","code":"// anyplot.ai\n// line-stepwise: Step Line Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 90, right: 60, bottom: 90, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: call-center staffing level over a 24h shift schedule -------------\n// Each point holds its agent count until the next shift change (step-after).\nconst staffing = [\n  { hour: 0, agents: 3 },\n  { hour: 6, agents: 3 },\n  { hour: 6, agents: 6 },\n  { hour: 9, agents: 6 },\n  { hour: 9, agents: 10 },\n  { hour: 12, agents: 10 },\n  { hour: 12, agents: 8 },\n  { hour: 14, agents: 8 },\n  { hour: 14, agents: 12 },\n  { hour: 18, agents: 12 },\n  { hour: 18, agents: 7 },\n  { hour: 22, agents: 7 },\n  { hour: 22, agents: 3 },\n  { hour: 24, agents: 3 },\n];\n// Vertices where the shift level actually changes (for the marker layer).\nconst changePoints = [\n  { hour: 0, agents: 3 },\n  { hour: 6, agents: 6 },\n  { hour: 9, agents: 10 },\n  { hour: 12, agents: 8 },\n  { hour: 14, agents: 12 },\n  { hour: 18, agents: 7 },\n  { hour: 22, agents: 3 },\n];\n\n// --- SVG mount ----------------------------------------------------------------\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// --- Scales ---------------------------------------------------------------\nconst x = d3.scaleLinear().domain([0, 24]).range([0, iw]);\nconst y = d3\n  .scaleLinear()\n  .domain([0, d3.max(staffing, (d) => d.agents)])\n  .nice()\n  .range([ih, 0]);\n\n// --- Y gridlines (subtle, data ink stays uncontested) ----------------------\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(y.ticks(6))\n  .join(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", (d) => y(d))\n  .attr(\"y2\", (d) => y(d))\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// --- Area fill under the step curve (brand color, low alpha) --------------\nconst area = d3\n  .area()\n  .curve(d3.curveStepAfter)\n  .x((d) => x(d.hour))\n  .y0(ih)\n  .y1((d) => y(d.agents));\n\ng.append(\"path\").datum(staffing).attr(\"d\", area).attr(\"fill\", t.palette[0]).attr(\"fill-opacity\", 0.12);\n\n// --- Step line ---------------------------------------------------------------\nconst line = d3\n  .line()\n  .curve(d3.curveStepAfter)\n  .x((d) => x(d.hour))\n  .y((d) => y(d.agents));\n\ng.append(\"path\")\n  .datum(staffing)\n  .attr(\"d\", line)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 3.5)\n  .attr(\"stroke-linejoin\", \"round\");\n\n// --- Markers at each shift-change vertex -----------------------------------\ng.selectAll(\"circle\")\n  .data(changePoints)\n  .join(\"circle\")\n  .attr(\"cx\", (d) => x(d.hour))\n  .attr(\"cy\", (d) => y(d.agents))\n  .attr(\"r\", 8)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2);\n\n// --- Peak callout (highlights the single highest staffing vertex) ---------\nconst peak = changePoints.reduce((a, b) => (b.agents > a.agents ? b : a));\ng.append(\"text\")\n  .attr(\"x\", x(peak.hour) + 14)\n  .attr(\"y\", y(peak.agents) - 10)\n  .attr(\"text-anchor\", \"start\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", \"600\")\n  .text(`Peak: ${peak.agents} agents`);\n\n// --- Axes -------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(\n    d3\n      .axisBottom(x)\n      .tickValues([0, 3, 6, 9, 12, 15, 18, 21, 24])\n      .tickFormat((d) => `${d}:00`),\n  );\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6));\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.inkSoft);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n// Hide the y-axis vertical domain line for a clean L-shape without a right/top frame.\nyAxis.select(\".domain\").remove();\nyAxis.selectAll(\"line\").remove();\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\", \"17px\")\n  .text(\"Hour of Day\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -80)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .text(\"Active Support Agents\");\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\", \"700\")\n  .text(\"line-stepwise · javascript · d3 · anyplot.ai\");\n"}