{"spec_id":"area-stacked-percent","library":"d3","language":"javascript","code":"// anyplot.ai\n// area-stacked-percent: 100% Stacked Area Chart\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\nconst inkMuted = t.theme === \"light\" ? \"#6B6A63\" : \"#A8A79F\";\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 140, right: 50, bottom: 70, left: 90 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Illustrative global desktop browser market share, 2015-2024 (yearly).\nconst SERIES = [\n  { key: \"chrome\", label: \"Chrome\" },\n  { key: \"safari\", label: \"Safari\" },\n  { key: \"edge\", label: \"Edge / IE\" },\n  { key: \"firefox\", label: \"Firefox\" },\n  { key: \"other\", label: \"Other\" },\n];\n\nconst data = [\n  { year: 2015, chrome: 47, safari: 7, edge: 21, firefox: 17, other: 8 },\n  { year: 2016, chrome: 52, safari: 7, edge: 18, firefox: 14, other: 9 },\n  { year: 2017, chrome: 57, safari: 8, edge: 14, firefox: 11, other: 10 },\n  { year: 2018, chrome: 61, safari: 8, edge: 11, firefox: 9, other: 11 },\n  { year: 2019, chrome: 64, safari: 9, edge: 8, firefox: 8, other: 11 },\n  { year: 2020, chrome: 66, safari: 9, edge: 7, firefox: 7, other: 11 },\n  { year: 2021, chrome: 65, safari: 9, edge: 6, firefox: 6, other: 14 },\n  { year: 2022, chrome: 64, safari: 9, edge: 6, firefox: 5, other: 16 },\n  { year: 2023, chrome: 63, safari: 10, edge: 5, firefox: 4, other: 18 },\n  { year: 2024, chrome: 61, safari: 10, edge: 5, firefox: 4, other: 20 },\n];\n\nconst years = data.map((d) => d.year);\n\n// \"Other\" reads as the semantic muted anchor; the named series keep canonical order.\nconst colorFor = (key) => {\n  if (key === \"other\") return inkMuted;\n  const index = SERIES.findIndex((s) => s.key === key);\n  return t.palette[index];\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(d3.extent(years)).range([0, iw]);\nconst y = d3.scaleLinear().domain([0, 1]).range([ih, 0]);\n\n// --- Stack (normalized to 100%) -----------------------------------------\nconst stack = d3.stack().keys(SERIES.map((s) => s.key)).offset(d3.stackOffsetExpand);\nconst layers = stack(data);\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(layers)\n  .join(\"path\")\n  .attr(\"class\", \"layer\")\n  .attr(\"fill\", (d) => colorFor(d.key))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"d\", area);\n\n// --- Gridlines (y-axis only, drawn above the opaque stack at low opacity) --\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(y.ticks(4))\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.pageBg)\n  .attr(\"stroke-width\", 1)\n  .attr(\"opacity\", 0.35);\n\n// --- Crossover annotation (Safari overtakes Edge/IE) -----------------------\nconst crossoverYear = 2019;\nconst crossoverRow = data.find((d) => d.year === crossoverYear);\nconst crossoverLayer = layers.find((l) => l.key === \"safari\");\nconst crossoverIndex = data.indexOf(crossoverRow);\nconst crossoverY = y((crossoverLayer[crossoverIndex][0] + crossoverLayer[crossoverIndex][1]) / 2);\n\ng.append(\"circle\")\n  .attr(\"cx\", x(crossoverYear))\n  .attr(\"cy\", crossoverY)\n  .attr(\"r\", 4)\n  .attr(\"fill\", t.pageBg)\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 1.5);\n\nfor (const [stroke, fill] of [\n  [t.pageBg, \"none\"],\n  [\"none\", t.ink],\n]) {\n  g.append(\"text\")\n    .attr(\"x\", x(crossoverYear))\n    .attr(\"y\", crossoverY - 14)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"stroke\", stroke)\n    .attr(\"stroke-width\", 3)\n    .attr(\"fill\", fill)\n    .style(\"font-size\", \"13px\")\n    .style(\"font-weight\", \"600\")\n    .text(\"Safari overtakes Edge/IE\");\n}\n\n// --- Axes --------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).tickValues(years).tickFormat(d3.format(\"d\")).tickSizeOuter(0));\n\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(4).tickFormat(d3.format(\".0%\")).tickSizeOuter(0));\n\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}\nxAxis.selectAll(\"line\").remove();\nyAxis.selectAll(\"line\").remove();\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -64)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Share of Total (%)\");\n\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 48)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Year\");\n\n// --- Legend (measured in-browser, then centered) ----------------------------\nconst legendG = svg.append(\"g\");\nconst swatch = 16;\nconst swatchGap = 8;\nconst itemGap = 28;\nlet cursorX = 0;\nconst items = [];\n\nfor (const s of SERIES) {\n  const item = legendG.append(\"g\");\n  item\n    .append(\"rect\")\n    .attr(\"width\", swatch)\n    .attr(\"height\", swatch)\n    .attr(\"rx\", 3)\n    .attr(\"fill\", colorFor(s.key));\n  const label = item\n    .append(\"text\")\n    .attr(\"x\", swatch + swatchGap)\n    .attr(\"y\", swatch - 3)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"15px\")\n    .text(s.label);\n  const itemWidth = swatch + swatchGap + label.node().getBBox().width;\n  items.push({ item, x: cursorX });\n  cursorX += itemWidth + itemGap;\n}\n\nconst legendWidth = cursorX - itemGap;\nconst legendStartX = (width - legendWidth) / 2;\nfor (const { item, x: itemX } of items) {\n  item.attr(\"transform\", `translate(${legendStartX + itemX},${92})`);\n}\n\n// --- Title -------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 44)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"area-stacked-percent · javascript · d3 · anyplot.ai\");\n"}