{"spec_id":"line-load-duration","library":"d3","language":"javascript","code":"// anyplot.ai\n// line-load-duration: Load Duration Curve for Energy Systems\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 92/100 | Created: 2026-06-10\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\nconst margin = { top: 95, right: 235, bottom: 82, left: 105 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// Deterministic LCG — no Math.random() in the browser harness\nlet seed = 42;\nconst lcg = () => {\n  seed = (seed * 1664525 + 1013904223) >>> 0;\n  return seed / 4294967296;\n};\n\n// Synthesize 8 760 hourly loads with seasonal + daily patterns, then sort descending\nconst raw = [];\nfor (let i = 0; i < 8760; i++) {\n  const doy = Math.floor(i / 24);\n  const hod = i % 24;\n  const seasonal = 220 * Math.cos((doy / 365) * 2 * Math.PI);\n  const daily = -150 * Math.cos((hod / 24) * 2 * Math.PI)\n              - 80  * Math.cos((hod / 24) * 4 * Math.PI - 0.5);\n  raw.push(800 + seasonal + daily + (lcg() - 0.5) * 80);\n}\nraw.sort((a, b) => b - a);\n\nconst data = raw.map((v, i) => ({\n  hour: i,\n  load_mw: Math.max(350, Math.min(1250, v)),\n}));\n\n// Capacity thresholds (MW) and statistics\nconst baseCap  = 580;\nconst interCap = 860;\nconst peakMax  = d3.max(data, d => d.load_mw);\nconst totalTWh = (data.reduce((s, d) => s + d.load_mw, 0) / 1e6).toFixed(1);\n\n// Boundary indices (hours at which the curve crosses each threshold)\nconst peakEnd  = data.filter(d => d.load_mw > interCap).length;\nconst interEnd = data.filter(d => d.load_mw > baseCap).length;\n\n// SVG scaffold\nconst svg = d3.select(\"#container\").append(\"svg\")\n  .attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\")\n  .attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// Scales\nconst x = d3.scaleLinear().domain([0, 8759]).range([0, iw]);\nconst y = d3.scaleLinear().domain([0, 1350]).range([ih, 0]);\n\n// Area generators for three load regions (Imprint palette positions 0 → 2)\nconst areaBase = d3.area()\n  .x(d => x(d.hour))\n  .y0(y(0))\n  .y1(d => y(Math.min(d.load_mw, baseCap)));\n\nconst areaInter = d3.area()\n  .x(d => x(d.hour))\n  .y0(y(baseCap))\n  .y1(d => y(Math.min(d.load_mw, interCap)))\n  .defined(d => d.load_mw > baseCap);\n\nconst areaPeak = d3.area()\n  .x(d => x(d.hour))\n  .y0(y(interCap))\n  .y1(d => y(d.load_mw))\n  .defined(d => d.load_mw > interCap);\n\n// Filled regions\ng.append(\"path\").datum(data).attr(\"d\", areaBase)\n  .attr(\"fill\", t.palette[0]).attr(\"fill-opacity\", 0.50);\ng.append(\"path\").datum(data).attr(\"d\", areaInter)\n  .attr(\"fill\", t.palette[1]).attr(\"fill-opacity\", 0.50);\ng.append(\"path\").datum(data).attr(\"d\", areaPeak)\n  .attr(\"fill\", t.palette[2]).attr(\"fill-opacity\", 0.50);\n\n// Horizontal gridlines (y-axis only, drawn on top of fills so they read through)\nconst gridG = g.append(\"g\");\ngridG.call(d3.axisLeft(y).ticks(7).tickSize(-iw).tickFormat(\"\"));\ngridG.select(\".domain\").remove();\ngridG.selectAll(\".tick line\").attr(\"stroke\", t.grid);\n\n// LDC curve\ng.append(\"path\").datum(data)\n  .attr(\"d\", d3.line().x(d => x(d.hour)).y(d => y(d.load_mw)))\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 2.5);\n\n// Capacity reference lines (dashed, color-matched to their region)\n[\n  [baseCap,  \"Base Load Cap.\",    t.palette[0]],\n  [interCap, \"Intermediate Cap.\", t.palette[1]],\n].forEach(([cap, label, color]) => {\n  g.append(\"line\")\n    .attr(\"x1\", 0).attr(\"x2\", iw)\n    .attr(\"y1\", y(cap)).attr(\"y2\", y(cap))\n    .attr(\"stroke\", color).attr(\"stroke-width\", 1.8)\n    .attr(\"stroke-dasharray\", \"10,6\");\n\n  g.append(\"text\")\n    .attr(\"x\", iw + 10).attr(\"y\", y(cap)).attr(\"dy\", \"0.35em\")\n    .attr(\"fill\", color)\n    .style(\"font-size\", \"13px\").style(\"font-weight\", \"500\")\n    .text(`${label}: ${cap} MW`);\n});\n\n// Axes\nconst xAx = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x)\n    .tickValues([0, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8760])\n    .tickFormat(d3.format(\",d\")));\n\nconst yAx = g.append(\"g\").call(d3.axisLeft(y).ticks(7));\n\n[xAx, yAx].forEach(ax => {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  ax.selectAll(\".tick line\").attr(\"stroke\", t.inkSoft);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n});\n\n// Axis labels\nsvg.append(\"text\")\n  .attr(\"x\", margin.left + iw / 2).attr(\"y\", height - 18)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\")\n  .text(\"Duration (hours)\");\n\nsvg.append(\"text\")\n  .attr(\"transform\", `translate(22,${margin.top + ih / 2})rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\")\n  .text(\"Load (MW)\");\n\n// Region labels (directly on the chart)\ng.append(\"text\")\n  .attr(\"x\", x(peakEnd / 2))\n  .attr(\"y\", y((interCap + peakMax) / 2))\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", \"14px\").style(\"font-weight\", \"700\")\n  .text(\"PEAK\");\n\ng.append(\"text\")\n  .attr(\"x\", x((peakEnd + interEnd) / 2))\n  .attr(\"y\", y((baseCap + interCap) / 2))\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", \"14px\").style(\"font-weight\", \"700\")\n  .text(\"INTERMEDIATE\");\n\ng.append(\"text\")\n  .attr(\"x\", x((interEnd + 8759) / 2))\n  .attr(\"y\", y(baseCap * 0.40))\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", \"14px\").style(\"font-weight\", \"700\")\n  .text(\"BASE LOAD\");\n\n// Total energy annotation in upper-right empty space\ng.append(\"text\")\n  .attr(\"x\", x(7300)).attr(\"y\", y(1120))\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\")\n  .text(`Annual Energy: ${totalTWh} TWh`);\n\n// Title\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 52)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", \"22px\").style(\"font-weight\", \"600\")\n  .text(\"line-load-duration · javascript · d3 · anyplot.ai\");\n"}