{"spec_id":"heatmap-stripes-climate","library":"muix","language":"javascript","code":"// anyplot.ai\n// heatmap-stripes-climate: Climate Warming Stripes\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 87/100 | Created: 2026-08-25\n//# anyplot-orientation: landscape\n// anyplot.ai\n// heatmap-stripes-climate: Climate Warming Stripes\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-08-25\nimport { BarChart } from \"@mui/x-charts/BarChart\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst tokens = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Synthetic global-mean temperature anomaly (°C, vs. a 1961-1990-style\n// baseline) for 1900-2024 — a tiny LCG stands in for a seeded RNG since the\n// browser has no Math.random() seed.\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1103515245 + 12345) & 0x7fffffff;\n    return state / 0x7fffffff;\n  };\n}\nconst random = lcg(42);\n\nconst startYear = 1900;\nconst endYear = 2024;\nconst years = [];\nconst anomalies = [];\nfor (let year = startYear; year <= endYear; year += 1) {\n  const progress = (year - startYear) / (endYear - startYear);\n  const trend = -0.25 + 1.35 * progress ** 2.2; // slow start, accelerating post-1975 warming\n  const noise = (random() - 0.5) * 0.28;\n  years.push(year);\n  anomalies.push(Math.round((trend + noise) * 100) / 100);\n}\nconst maxAbsAnomaly = Math.max(...anomalies.map(Math.abs));\n\n// --- Diverging color scale (Imprint imprint_div: cold blue -> midpoint -> warm red) ---\n// Bar height stays constant (see `heights` below) — only color encodes the\n// anomaly, the defining trait of the warming-stripes form.\nfunction hexToRgb(hex) {\n  const clean = hex.replace(\"#\", \"\");\n  return [parseInt(clean.slice(0, 2), 16), parseInt(clean.slice(2, 4), 16), parseInt(clean.slice(4, 6), 16)];\n}\nfunction mixColor(from, to, fraction) {\n  const [r1, g1, b1] = hexToRgb(from);\n  const [r2, g2, b2] = hexToRgb(to);\n  const r = Math.round(r1 + (r2 - r1) * fraction);\n  const g = Math.round(g1 + (g2 - g1) * fraction);\n  const b = Math.round(b1 + (b2 - b1) * fraction);\n  return `rgb(${r}, ${g}, ${b})`;\n}\nconst COLD = tokens.div[2]; // Imprint imprint_div — blue, cold anomalies\nconst MID = tokens.div[1]; // Imprint imprint_div — theme-adaptive midpoint (zero anomaly)\nconst HOT = tokens.div[0]; // Imprint imprint_div — matte red, warm anomalies\n// The diverging midpoint token is the theme-adaptive page background, so a\n// fraction that reaches exactly 0 mixes to a color pixel-identical to the\n// backdrop and the bar disappears. Floor the magnitude so every bar keeps a\n// faint but real, non-zero-contrast tint — the fixed red/blue endpoints are\n// untouched since |fraction| is already >= this floor near them.\nconst MIN_FRACTION = 0.15;\nfunction anomalyToColor(value) {\n  const rawFraction = value / maxAbsAnomaly; // symmetric around zero, per spec\n  const sign = rawFraction < 0 ? -1 : 1;\n  const fraction = sign * Math.max(MIN_FRACTION, Math.abs(rawFraction));\n  return fraction < 0 ? mixColor(MID, COLD, -fraction) : mixColor(MID, HOT, fraction);\n}\nconst stripeColors = anomalies.map(anomalyToColor);\n\n// Uniform bar height — the stripe form encodes the anomaly purely through\n// color, never through height, so every bar spans the full drawing area.\nconst heights = years.map(() => 1);\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  const stripeWidth = Math.round(window.ANYPLOT_SIZE.width * 0.94);\n  const stripeHeight = Math.round(stripeWidth / 3.1); // ~3:1, per spec's \"wide and short\"\n\n  return (\n    <Box\n      sx={{\n        width: window.ANYPLOT_SIZE.width,\n        height: window.ANYPLOT_SIZE.height,\n        display: \"flex\",\n        flexDirection: \"column\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n      }}\n    >\n      <Typography sx={{ fontSize: 22, fontWeight: 500, mb: 3 }}>\n        heatmap-stripes-climate · javascript · muix · anyplot.ai\n      </Typography>\n      <BarChart\n        width={stripeWidth}\n        height={stripeHeight}\n        series={[\n          {\n            data: heights,\n            color: tokens.palette[0],\n            valueFormatter: (_value, context) =>\n              `${years[context.dataIndex]}: ${anomalies[context.dataIndex].toFixed(2)}°C`,\n          },\n        ]}\n        xAxis={[\n          {\n            scaleType: \"band\",\n            data: years,\n            categoryGapRatio: 0,\n            colorMap: { type: \"ordinal\", values: years, colors: stripeColors },\n          },\n        ]}\n        yAxis={[{ scaleType: \"linear\", min: 0, max: 1, domainLimit: \"strict\" }]}\n        bottomAxis={null}\n        leftAxis={null}\n        margin={{ top: 0, right: 0, bottom: 0, left: 0 }}\n        borderRadius={0}\n        skipAnimation\n        slotProps={{ legend: { hidden: true } }}\n      />\n    </Box>\n  );\n}\n"}