{"spec_id":"heatmap-stripes-climate","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// heatmap-stripes-climate: Climate Warming Stripes\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 85/100 | Created: 2026-08-25\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Synthetic global-mean temperature anomaly, 1850-2024, relative to a\n// 1961-1990 baseline. An accelerating warming trend plus small year-to-year\n// noise from a fixed-seed LCG (the browser has no seeded Math.random).\nlet seed = 42;\nconst lcgRandom = () => {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n};\n\nconst startYear = 1850;\nconst endYear = 2024;\nconst years = [];\nfor (let year = startYear; year <= endYear; year += 1) years.push(year);\n\nconst anomalies = years.map((year) => {\n  const progress = (year - startYear) / (endYear - startYear);\n  const trend = -0.45 + 1.65 * Math.pow(progress, 1.8);\n  const noise = (lcgRandom() - 0.5) * 0.3;\n  return Math.round((trend + noise) * 100) / 100;\n});\n\n// Symmetric color scale around zero so equal +/- anomalies match in intensity.\n// Domain convention (see style guide \"Semantic exception\"): cold -> blue,\n// warm -> red, so the diverging stops are used in reverse order (t.div is\n// [red, neutral, blue]).\nconst maxAbsAnomaly = Math.max(...anomalies.map(Math.abs));\n\nconst rgbToHex = (rgb) =>\n  `#${rgb.map((c) => Math.round(c).toString(16).padStart(2, \"0\")).join(\"\")}`;\nconst lerpColor = (colorA, colorB, ratio) => {\n  const parse = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));\n  const a = parse(colorA);\n  const b = parse(colorB);\n  return rgbToHex(a.map((v, i) => v + (b[i] - v) * ratio));\n};\n\n// column series has no built-in colorAxis coloring (that composition ships\n// only with the heatmap/treemap modules, which are not loaded), so each\n// bar's color is computed directly from the diverging Imprint stops.\n// The blend anchor is the midpoint of the two poles (t.div[0]/t.div[2]),\n// not t.div[1] (the page-background-adaptive midpoint): t.div[1] is\n// #FAF8F1 on light / #1A1A17 on dark, so blending toward it would make\n// every low/medium-magnitude year render a different color per theme.\n// The pole midpoint is theme-independent, so a given anomaly reads as the\n// same color in both renders, matching the \"data colors are identical,\n// only chrome flips\" rule.\n// Magnitude is floored at 10% so a near-zero anomaly still blends slightly\n// toward its pole instead of landing on the exact neutral color (which\n// would make that year hard to distinguish from its neighbors).\nconst MIN_TINT = 0.1;\nconst neutral = lerpColor(t.div[2], t.div[0], 0.5);\nconst colorForAnomaly = (value) => {\n  const cold = t.div[2];\n  const warm = t.div[0];\n  const magnitude = Math.max(Math.abs(value) / maxAbsAnomaly, MIN_TINT);\n  return value >= 0 ? lerpColor(neutral, warm, magnitude) : lerpColor(neutral, cold, magnitude);\n};\n\nconst stripes = years.map((year, i) => ({\n  x: i,\n  y: 1,\n  color: colorForAnomaly(anomalies[i]),\n}));\n\n// Subtle radial vignette drawn with the SVGRenderer (a genuinely\n// Highcharts-distinctive capability, not available via plain CSS on the\n// mount node) so the stripe field reads with a touch more depth instead of\n// flat edge-to-edge color, without adding any forbidden chrome. Fixed to a\n// single ink-based tint (not theme-branched) and confined to the plot box\n// (excludes the title band) so it never touches title legibility and never\n// breaks the required pixel-identical data colors across themes.\nconst drawVignette = function () {\n  const { plotLeft, plotTop, plotWidth, plotHeight } = this;\n  this.renderer\n    .rect(plotLeft, plotTop, plotWidth, plotHeight)\n    .attr({\n      fill: {\n        radialGradient: { cx: 0.5, cy: 0.5, r: 0.75 },\n        stops: [\n          [0, \"rgba(26,26,23,0)\"],\n          [1, \"rgba(26,26,23,0.14)\"],\n        ],\n      },\n      zIndex: 6,\n    })\n    .add();\n};\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"column\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    spacing: [40, 0, 0, 0],\n    style: { fontFamily: \"inherit\" },\n    events: { load: drawVignette },\n  },\n  credits: { enabled: false },\n  title: {\n    text: \"heatmap-stripes-climate · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  xAxis: {\n    visible: false,\n    min: 0,\n    max: years.length - 1,\n    minPadding: 0,\n    maxPadding: 0,\n  },\n  yAxis: {\n    visible: false,\n    min: 0,\n    max: 1,\n    minPadding: 0,\n    maxPadding: 0,\n  },\n  legend: { enabled: false },\n  tooltip: { enabled: false },\n  plotOptions: {\n    series: { animation: false },\n    column: {\n      pointPadding: 0,\n      groupPadding: 0,\n      borderWidth: 0,\n      borderRadius: 0,\n    },\n  },\n  series: [\n    {\n      name: \"Temperature anomaly (°C)\",\n      data: stripes,\n    },\n  ],\n});\n"}