{"spec_id":"choropleth-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// choropleth-basic: Choropleth Map with Regional Coloring\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 86/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\nconst MUTED = t.theme === \"dark\" ? \"#A8A79F\" : \"#6B6A63\"; // adaptive \"other / no data\" anchor — not in ANYPLOT_TOKENS, mirrors default-style-guide.md\n\n// --- Layout constants --------------------------------------------------------\n// Only the core Highcharts bundle is loaded (no highmaps / mapChart series —\n// see prompts/library/highcharts.md), so there is no built-in geographic\n// projection or country topology to draw from. Region outlines below are\n// hand-authored, deliberately coarse polygons (world-region silhouettes, not\n// surveyed coastlines) plotted through ordinary linear lon/lat axes — a simple\n// equirectangular-style projection — and filled with SVGRenderer paths. This\n// mirrors the technique already used by this library's own\n// scatter-map-geographic and heatmap-geographic implementations for the same\n// \"no highmaps module\" constraint.\nconst MOUNT_WIDTH = 1600;\nconst MOUNT_HEIGHT = 900;\nconst MARGIN_LEFT = 40;\nconst MARGIN_RIGHT = 40;\nconst MARGIN_TOP = 115;\nconst MARGIN_BOTTOM = 140;\nconst PLOT_WIDTH = MOUNT_WIDTH - MARGIN_LEFT - MARGIN_RIGHT;\nconst PLOT_HEIGHT = MOUNT_HEIGHT - MARGIN_TOP - MARGIN_BOTTOM;\n\n// LON_MIN/LON_MAX are asymmetric on purpose: the visible region mass (North\n// America through Oceania) spans roughly lon -170..153, not a symmetric\n// -170..170 — Antarctica's single eastward vertex at lon 170 is a thin sliver\n// that barely registers visually. Padding both edges by the same 25° keeps\n// the *content* horizontally centered instead of the raw coordinate extremes.\nconst LON_MIN = -195;\nconst LON_MAX = 178;\nconst LAT_MIN = -85;\nconst LAT_MAX = 80;\nconst PX_PER_LON = PLOT_WIDTH / (LON_MAX - LON_MIN);\nconst PX_PER_LAT = PLOT_HEIGHT / (LAT_MAX - LAT_MIN);\n\n// --- Data: renewable share of electricity generation by world region (%) ---\n// Illustrative figures (not sourced from a specific publication) chosen to\n// respect real-world ordering: hydro-heavy South America highest, oil/gas\n// exporting Middle East lowest. `value: null` demonstrates the spec's\n// \"handle missing data gracefully\" requirement (Antarctica has no grid).\nconst REGIONS = [\n  {\n    name: \"North America\",\n    value: 24,\n    poly: [[-170, 72], [-140, 72], [-95, 68], [-70, 50], [-55, 45], [-65, 25], [-100, 25], [-125, 32], [-140, 50], [-170, 60]],\n  },\n  {\n    name: \"Central America & Caribbean\",\n    value: 38,\n    poly: [[-105, 25], [-97, 25], [-90, 15], [-80, 9], [-77, 8], [-70, 10], [-65, 18], [-80, 23], [-95, 22]],\n  },\n  {\n    name: \"South America\",\n    value: 55,\n    poly: [[-80, 12], [-60, 10], [-50, 5], [-35, -5], [-40, -23], [-58, -35], [-68, -55], [-75, -50], [-80, -18], [-82, 0]],\n  },\n  {\n    name: \"Western Europe\",\n    value: 44,\n    poly: [[-10, 44], [-5, 52], [5, 60], [15, 58], [10, 47], [15, 42], [8, 36], [-5, 38]],\n  },\n  {\n    name: \"Eastern Europe\",\n    value: 26,\n    poly: [[15, 50], [20, 60], [40, 58], [40, 45], [28, 42], [18, 44]],\n  },\n  {\n    name: \"North Africa\",\n    value: 10,\n    poly: [[-17, 21], [-10, 35], [10, 37], [25, 32], [35, 22], [30, 15], [10, 15], [-10, 15]],\n  },\n  {\n    name: \"Sub-Saharan Africa\",\n    value: 20,\n    poly: [[-18, 15], [10, 15], [30, 15], [42, 10], [40, -15], [30, -30], [18, -35], [10, -28], [-10, -5]],\n  },\n  {\n    name: \"Middle East\",\n    value: 5,\n    poly: [[30, 32], [36, 42], [48, 42], [63, 25], [55, 12], [42, 15], [35, 22]],\n  },\n  {\n    name: \"Central Asia\",\n    value: 12,\n    poly: [[46, 55], [66, 56], [76, 48], [70, 38], [58, 37], [46, 41]],\n  },\n  {\n    name: \"South Asia\",\n    value: 18,\n    poly: [[60, 36], [76, 33], [86, 27], [90, 21], [79, 8], [68, 8], [60, 23]],\n  },\n  {\n    name: \"East Asia\",\n    value: 29,\n    poly: [[84, 39], [92, 52], [120, 53], [135, 45], [130, 32], [122, 25], [108, 18], [96, 21], [86, 27]],\n  },\n  {\n    name: \"Southeast Asia\",\n    value: 24,\n    poly: [[92, 22], [105, 23], [110, 10], [120, 18], [125, 10], [141, -2], [130, -8], [112, -8], [98, 3], [92, 10]],\n  },\n  {\n    name: \"Oceania\",\n    value: 30,\n    poly: [[113, -22], [130, -12], [142, -11], [153, -27], [145, -38], [137, -35], [113, -32]],\n  },\n  {\n    name: \"Antarctica\",\n    value: null,\n    poly: [[-30, -62], [30, -62], [70, -65], [110, -68], [150, -65], [170, -70], [130, -78], [60, -80], [-20, -78], [-60, -70]],\n  },\n];\n\nconst VALUE_MIN = 0;\nconst VALUE_MAX = 60;\n\n// --- Helpers: sequential color scale + geometry ------------------------------\nfunction hexToRgb(hex) {\n  const n = parseInt(hex.slice(1), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\nfunction lerpColor(hexA, hexB, frac) {\n  const a = hexToRgb(hexA);\n  const b = hexToRgb(hexB);\n  const rgb = a.map((c, i) => Math.round(c + (b[i] - c) * frac));\n  return `#${rgb.map((v) => v.toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\nfunction colorForValue(value) {\n  const frac = Math.min(1, Math.max(0, (value - VALUE_MIN) / (VALUE_MAX - VALUE_MIN)));\n  return lerpColor(t.seq[0], t.seq[1], frac);\n}\n// Precompute centroid + a hover hit-radius (px) sized to fit inside each\n// region's bounding box, so the invisible tooltip marker (below) never spills\n// past its own polygon into a neighbor.\nREGIONS.forEach((region) => {\n  const lons = region.poly.map((p) => p[0]);\n  const lats = region.poly.map((p) => p[1]);\n  const minLon = Math.min(...lons);\n  const maxLon = Math.max(...lons);\n  const minLat = Math.min(...lats);\n  const maxLat = Math.max(...lats);\n  region.centroidLon = (minLon + maxLon) / 2;\n  region.centroidLat = (minLat + maxLat) / 2;\n  region.hitRadius = 0.42 * Math.min((maxLon - minLon) * PX_PER_LON, (maxLat - minLat) * PX_PER_LAT);\n  region.fill = region.value === null ? MUTED : colorForValue(region.value);\n});\n\n// --- Region fills + boundaries (SVGRenderer, since highmaps is off-limits) --\nfunction drawChoropleth(chart) {\n  const xAxis = chart.xAxis[0];\n  const yAxis = chart.yAxis[0];\n  const toPath = (poly) =>\n    poly.map((pt, i) => (i === 0 ? [\"M\", xAxis.toPixels(pt[0]), yAxis.toPixels(pt[1])] : [\"L\", xAxis.toPixels(pt[0]), yAxis.toPixels(pt[1])])).concat([[\"Z\"]]);\n\n  REGIONS.forEach((region) => {\n    const attrs = {\n      fill: region.fill,\n      stroke: t.pageBg,\n      \"stroke-width\": 2,\n      zIndex: 1,\n    };\n    if (region.value === null) {\n      attrs[\"stroke-dasharray\"] = \"6,4\";\n      attrs.stroke = MUTED;\n    }\n    chart.renderer.path(toPath(region.poly)).attr(attrs).add();\n  });\n\n  // \"No data\" label sits inside Antarctica's own silhouette — factual chrome\n  // identifying the missing-data region, not a storytelling callout.\n  const noData = REGIONS.find((r) => r.value === null);\n  chart.renderer\n    .text(\"No data\", xAxis.toPixels(noData.centroidLon), yAxis.toPixels(noData.centroidLat))\n    .attr({ align: \"center\", zIndex: 2 })\n    .css({ color: t.pageBg, fontSize: \"12px\", fontWeight: \"600\" })\n    .add();\n}\n\n// --- Manual color-scale legend (no colorAxis module in the core bundle) -----\nfunction drawLegend(chart) {\n  const r = chart.renderer;\n  const x0 = chart.plotLeft;\n  const y0 = chart.plotTop + chart.plotHeight + 46;\n  const barWidth = 280;\n  const barHeight = 16;\n\n  r.text(\"Renewable electricity generation share\", x0, y0 - 12)\n    .css({ color: t.inkSoft, fontSize: \"14px\", fontWeight: \"600\" })\n    .add();\n\n  r.rect(x0, y0, barWidth, barHeight, 3)\n    .attr({\n      fill: { linearGradient: { x1: 0, y1: 0, x2: 1, y2: 0 }, stops: [[0, t.seq[0]], [1, t.seq[1]]] },\n      \"stroke-width\": 0,\n    })\n    .add();\n  [\n    [0, `${VALUE_MIN}%`],\n    [0.5, `${(VALUE_MIN + VALUE_MAX) / 2}%`],\n    [1, `${VALUE_MAX}%+`],\n  ].forEach(([frac, label]) => {\n    r.text(label, x0 + frac * barWidth, y0 + barHeight + 20)\n      .attr({ align: frac === 0 ? \"left\" : frac === 1 ? \"right\" : \"center\" })\n      .css({ color: t.inkSoft, fontSize: \"12px\" })\n      .add();\n  });\n\n  // \"No data\" swatch, matching the dashed Antarctica boundary style.\n  const swatchX = x0 + barWidth + 60;\n  r.rect(swatchX, y0, 22, barHeight, 2).attr({ fill: MUTED, stroke: MUTED, \"stroke-width\": 1, \"stroke-dasharray\": \"4,3\" }).add();\n  r.text(\"No data\", swatchX + 30, y0 + barHeight - 3).css({ color: t.inkSoft, fontSize: \"12px\" }).add();\n}\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\n  \"container\",\n  {\n    chart: {\n      type: \"scatter\",\n      backgroundColor: \"transparent\",\n      animation: false,\n      style: { fontFamily: \"inherit\" },\n      marginLeft: MARGIN_LEFT,\n      marginRight: MARGIN_RIGHT,\n      marginTop: MARGIN_TOP,\n      marginBottom: MARGIN_BOTTOM,\n    },\n    credits: { enabled: false },\n    colors: t.palette,\n    title: {\n      text: \"choropleth-basic · javascript · highcharts · anyplot.ai\",\n      align: \"left\",\n      style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n    },\n    subtitle: {\n      text: \"Renewable share of electricity generation by world region — illustrative data\",\n      align: \"left\",\n      style: { color: t.inkSoft, fontSize: \"14px\" },\n    },\n    xAxis: {\n      min: LON_MIN,\n      max: LON_MAX,\n      visible: false,\n    },\n    yAxis: {\n      min: LAT_MIN,\n      max: LAT_MAX,\n      visible: false,\n      title: { text: null },\n    },\n    legend: { enabled: false },\n    tooltip: {\n      backgroundColor: t.elevatedBg,\n      borderColor: t.inkSoft,\n      style: { color: t.ink, fontSize: \"13px\" },\n      pointFormatter: function () {\n        return this.custom.value === null ? `<b>${this.custom.name}</b><br/>No data available` : `<b>${this.custom.name}</b><br/>Renewable share: ${this.custom.value}%`;\n      },\n    },\n    plotOptions: {\n      series: { animation: false },\n      scatter: { enableMouseTracking: true, states: { hover: { enabled: false } } },\n    },\n    series: [\n      {\n        name: \"Regions\",\n        type: \"scatter\",\n        showInLegend: false,\n        zIndex: 3,\n        data: REGIONS.map((region) => ({\n          x: region.centroidLon,\n          y: region.centroidLat,\n          custom: { name: region.name, value: region.value },\n          marker: { radius: region.hitRadius, fillColor: \"rgba(0,0,0,0)\", lineWidth: 0 },\n        })),\n      },\n    ],\n  },\n  function (chart) {\n    drawChoropleth(chart);\n    drawLegend(chart);\n  },\n);\n"}