{"spec_id":"area-stacked","library":"d3","language":"javascript","code":"// anyplot.ai\n// area-stacked: Stacked Area Chart\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-08-17\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 110, right: 280, bottom: 80, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: Germany energy consumption by sector, 2010-2024 (TWh) -----------\n// Ordered largest series first so it lands at the base of the stack.\nconst sectors = [\n  { key: \"industrial\", label: \"Industrial\",\n    values: [420, 425, 430, 438, 445, 450, 455, 460, 468, 475, 480, 486, 492, 498, 505] },\n  { key: \"transportation\", label: \"Transportation\",\n    values: [310, 315, 322, 328, 335, 340, 346, 352, 358, 363, 368, 372, 376, 380, 384] },\n  { key: \"commercial\", label: \"Commercial\",\n    values: [210, 214, 219, 224, 229, 234, 239, 244, 249, 253, 257, 261, 265, 269, 273] },\n  { key: \"residential\", label: \"Residential\",\n    values: [180, 182, 185, 188, 191, 194, 197, 200, 203, 206, 209, 212, 215, 218, 221] },\n  { key: \"agriculture\", label: \"Agriculture\",\n    values: [60, 61, 62, 63, 64, 66, 67, 68, 70, 71, 73, 74, 76, 77, 79] },\n];\nconst years = d3.range(2010, 2025);\nconst data = years.map((year, i) => {\n  const row = { year };\n  sectors.forEach((s) => (row[s.key] = s.values[i]));\n  return row;\n});\nconst keys = sectors.map((s) => s.key);\nconst series = d3.stack().keys(keys)(data);\n\n// --- Scales -------------------------------------------------------------\nconst x = d3.scaleLinear().domain(d3.extent(years)).range([0, iw]);\nconst yMax = d3.max(series, (layer) => d3.max(layer, (d) => d[1]));\nconst y = d3.scaleLinear().domain([0, yMax]).nice().range([ih, 0]);\nconst color = d3.scaleOrdinal().domain(keys).range(t.palette);\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// --- Gridlines (y-axis only) ------------------------------------------------\ng.append(\"g\")\n  .call(d3.axisLeft(y).ticks(6).tickSize(-iw).tickFormat(\"\"))\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\ng.select(\".domain\").remove();\n\n// --- Areas ------------------------------------------------------------------\nconst area = d3\n  .area()\n  .x((d) => x(d.data.year))\n  .y0((d) => y(d[0]))\n  .y1((d) => y(d[1]))\n  .curve(d3.curveMonotoneX);\n\ng.selectAll(\"path.layer\")\n  .data(series)\n  .join(\"path\")\n  .attr(\"class\", \"layer\")\n  .attr(\"fill\", (d) => color(d.key))\n  .attr(\"fill-opacity\", 0.88)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"d\", area);\n\n// --- Direct end-of-series value labels (color-matched to each layer) --------\n// Distinctive-D3 touch: read each layer's final band height straight off the\n// stack datum instead of leaning on the legend alone for value lookup.\nconst lastIdx = data.length - 1;\ng.selectAll(\"text.end-label\")\n  .data(series)\n  .join(\"text\")\n  .attr(\"class\", \"end-label\")\n  .attr(\"x\", iw + 8)\n  .attr(\"y\", (d) => y((d[lastIdx][0] + d[lastIdx][1]) / 2) + 4)\n  .attr(\"fill\", (d) => color(d.key))\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 3)\n  .attr(\"stroke-linejoin\", \"round\")\n  .style(\"paint-order\", \"stroke\")\n  .style(\"font-size\", \"13px\")\n  .style(\"font-weight\", \"600\")\n  .text((d) => d3.format(\",\")(d[lastIdx][1] - d[lastIdx][0]));\n\n// --- Axes -------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(8).tickFormat(d3.format(\"d\")));\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\n// --- Axis labels --------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Year\");\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\", \"16px\")\n  .text(\"Energy Consumption (TWh)\");\n\n// --- Legend -------------------------------------------------------------\nconst legend = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${width - margin.right + 55},${margin.top + 10})`);\nconst legendItem = legend\n  .selectAll(\"g\")\n  .data(sectors)\n  .join(\"g\")\n  .attr(\"transform\", (d, i) => `translate(0,${i * 34})`);\nlegendItem\n  .append(\"rect\")\n  .attr(\"width\", 18)\n  .attr(\"height\", 18)\n  .attr(\"rx\", 3)\n  .attr(\"fill\", (d) => color(d.key));\nlegendItem\n  .append(\"text\")\n  .attr(\"x\", 26)\n  .attr(\"y\", 14)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text((d) => d.label);\n\n// --- Growth annotation (data storytelling callout) ---------------------\n// Reads the total straight off the topmost stack layer so it stays correct\n// if the underlying data changes.\nconst totalStart = series[series.length - 1][0][1];\nconst totalEnd = series[series.length - 1][lastIdx][1];\nconst growthPct = Math.round(((totalEnd - totalStart) / totalStart) * 100);\n\n// Which sector drove the most absolute growth — a second layer of insight\n// beyond the aggregate total, read straight off each sector's own values.\nconst topGrower = sectors.reduce((best, s) => {\n  const gain = s.values[s.values.length - 1] - s.values[0];\n  const pct = Math.round((gain / s.values[0]) * 100);\n  return gain > best.gain ? { label: s.label, gain, pct } : best;\n}, { label: \"\", gain: -Infinity, pct: 0 });\n\nconst callout = g.append(\"g\").attr(\"transform\", \"translate(8,8)\");\ncallout\n  .append(\"rect\")\n  .attr(\"width\", 320)\n  .attr(\"height\", 80)\n  .attr(\"rx\", 8)\n  .attr(\"fill\", t.elevatedBg)\n  .attr(\"stroke\", t.grid);\ncallout\n  .append(\"text\")\n  .attr(\"x\", 16)\n  .attr(\"y\", 24)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text(\"Total energy consumption\");\ncallout\n  .append(\"text\")\n  .attr(\"x\", 16)\n  .attr(\"y\", 46)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .style(\"font-weight\", \"600\")\n  .text(`${d3.format(\",\")(totalStart)} → ${d3.format(\",\")(totalEnd)} TWh (+${growthPct}%)`);\ncallout\n  .append(\"text\")\n  .attr(\"x\", 16)\n  .attr(\"y\", 68)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text(`${topGrower.label} led the gain: +${d3.format(\",\")(topGrower.gain)} TWh (+${topGrower.pct}%)`);\n\n// --- Title --------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 52)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"32px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"area-stacked · javascript · d3 · anyplot.ai\");\n"}