{"spec_id":"heatmap-stripes-climate","library":"d3","language":"javascript","code":"// anyplot.ai\n// heatmap-stripes-climate: Climate Warming Stripes\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 95/100 | Created: 2026-08-25\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst titleArea = 64;\n\n// --- Data: global mean temperature anomaly, 1850-2024, °C vs 1961-1990 baseline\n// Deterministic LCG (browser has no seeded Math.random) -----------------------\nlet seed = 42;\nfunction lcgNoise() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296 - 0.5;\n}\n\nconst startYear = 1850;\nconst endYear = 2024;\nconst years = d3.range(startYear, endYear + 1);\nconst anomalies = years.map((year) => {\n  const progress = (year - startYear) / (endYear - startYear);\n  // Long-term warming trend: slow pre-industrial drift, sharp post-1980 rise.\n  const trend = -0.35 + 1.55 * Math.pow(progress, 1.7);\n  const noise = lcgNoise() * 0.24;\n  return trend + noise;\n});\n\n// --- SVG mount ----------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\n// --- Color scale: symmetric diverging, cold->blue, warm->red (Imprint stops) --\n// Semantic exception (temperature hot->red / cold->blue) — reorder the Imprint\n// diverging stops so negative anomalies read blue and positive anomalies read red.\nconst extent = d3.max(anomalies, (d) => Math.abs(d));\nconst stripeColor = d3\n  .scaleSequential(d3.interpolateRgbBasis([t.div[2], t.div[1], t.div[0]]))\n  .domain([-extent, extent]);\n\n// --- Stripes: equal-width bands, no gaps, edge-to-edge --------------------------\n// Rendered as a single defs-based linearGradient (D3 data-join of <stop>\n// elements) rather than per-bar solid fills — a more SVG/D3-native way to\n// express a continuous, data-driven color transition.\nconst x = d3.scaleBand().domain(years).range([0, width]).padding(0);\nconst stripeTop = titleArea;\nconst stripeHeight = height - titleArea;\n\nconst stops = years.flatMap((year, i) => {\n  const color = stripeColor(anomalies[i]);\n  const startPct = (x(year) / width) * 100;\n  const endPct = ((x(year) + x.bandwidth()) / width) * 100;\n  return [\n    { offset: startPct, color },\n    { offset: endPct, color },\n  ];\n});\n\nsvg\n  .append(\"defs\")\n  .append(\"linearGradient\")\n  .attr(\"id\", \"stripe-gradient\")\n  .attr(\"x1\", \"0%\")\n  .attr(\"x2\", \"100%\")\n  .attr(\"y1\", \"0%\")\n  .attr(\"y2\", \"0%\")\n  .selectAll(\"stop\")\n  .data(stops)\n  .join(\"stop\")\n  .attr(\"offset\", (d) => `${d.offset}%`)\n  .attr(\"stop-color\", (d) => d.color);\n\nsvg\n  .append(\"rect\")\n  .attr(\"x\", 0)\n  .attr(\"y\", stripeTop)\n  .attr(\"width\", width)\n  .attr(\"height\", stripeHeight)\n  .attr(\"fill\", \"url(#stripe-gradient)\");\n\n// --- Record-warmest emphasis: subtle chrome-only outline, no new data encoding -\n// Adds a light storytelling layer (the single hottest year on record) without\n// introducing axes, labels, or gridlines; the outline uses theme ink, so it\n// flips between light/dark like the rest of the chrome while the data fill\n// underneath stays pixel-identical across themes.\nconst recordHotIndex = d3.maxIndex(anomalies);\nsvg\n  .append(\"rect\")\n  .attr(\"x\", x(years[recordHotIndex]))\n  .attr(\"y\", stripeTop)\n  .attr(\"width\", x.bandwidth())\n  .attr(\"height\", stripeHeight)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-opacity\", 0.35)\n  .attr(\"stroke-width\", 2);\n\n// --- Title ----------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 40)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"20px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"heatmap-stripes-climate · javascript · d3 · anyplot.ai\");\n"}