{"spec_id":"bubble-map-geographic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// bubble-map-geographic: Bubble Map with Sized Geographic Markers\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 86/100 | Created: 2026-09-01\n\nconst t = window.ANYPLOT_TOKENS;\nconst THEME = window.ANYPLOT_THEME;\n\n// --- Basemap chrome (not data — Imprint palette only governs data colors) --\nconst LAND = THEME === \"light\" ? \"#E4E0D2\" : \"#33332C\";\nconst LAND_EDGE = THEME === \"light\" ? \"#A79F8A\" : \"#4A4A40\";\nconst OCEAN = THEME === \"light\" ? \"#CFE3EF\" : \"#16222E\";\n\n// --- Data: significant earthquakes, sized by magnitude, colored by the ------\n// --- tectonic boundary type that produced them ------------------------------\nconst earthquakes = [\n  {\n    name: \"San Francisco, USA (1906)\",\n    lat: 37.77,\n    lon: -122.42,\n    magnitude: 7.9,\n    boundary: \"Transform\",\n  },\n  {\n    name: \"Tokyo, Japan (1923)\",\n    lat: 35.68,\n    lon: 139.69,\n    magnitude: 7.9,\n    boundary: \"Convergent\",\n  },\n  {\n    name: \"Valdivia, Chile (1960)\",\n    lat: -39.83,\n    lon: -73.05,\n    magnitude: 9.5,\n    boundary: \"Convergent\",\n  },\n  {\n    name: \"Anchorage, USA (1964)\",\n    lat: 61.02,\n    lon: -147.65,\n    magnitude: 9.2,\n    boundary: \"Convergent\",\n  },\n  {\n    name: \"Aceh, Indonesia (2004)\",\n    lat: 3.3,\n    lon: 95.98,\n    magnitude: 9.1,\n    boundary: \"Convergent\",\n  },\n  {\n    name: \"Kashmir, Pakistan (2005)\",\n    lat: 34.49,\n    lon: 73.63,\n    magnitude: 7.6,\n    boundary: \"Convergent\",\n  },\n  {\n    name: \"Sichuan, China (2008)\",\n    lat: 31.0,\n    lon: 103.32,\n    magnitude: 7.9,\n    boundary: \"Convergent\",\n  },\n  {\n    name: \"Port-au-Prince, Haiti (2010)\",\n    lat: 18.46,\n    lon: -72.53,\n    magnitude: 7.0,\n    boundary: \"Transform\",\n  },\n  {\n    name: \"Maule, Chile (2010)\",\n    lat: -35.85,\n    lon: -72.72,\n    magnitude: 8.8,\n    boundary: \"Convergent\",\n  },\n  {\n    name: \"Christchurch, New Zealand (2011)\",\n    lat: -43.53,\n    lon: 172.64,\n    magnitude: 6.3,\n    boundary: \"Transform\",\n  },\n  {\n    name: \"Tohoku, Japan (2011)\",\n    lat: 38.3,\n    lon: 142.37,\n    magnitude: 9.1,\n    boundary: \"Convergent\",\n  },\n  {\n    name: \"Gorkha, Nepal (2015)\",\n    lat: 28.23,\n    lon: 84.73,\n    magnitude: 7.8,\n    boundary: \"Convergent\",\n  },\n  {\n    name: \"Muisne, Ecuador (2016)\",\n    lat: 0.38,\n    lon: -79.94,\n    magnitude: 7.8,\n    boundary: \"Convergent\",\n  },\n  {\n    name: \"Puebla, Mexico (2017)\",\n    lat: 18.4,\n    lon: -98.72,\n    magnitude: 7.1,\n    boundary: \"Convergent\",\n  },\n  {\n    name: \"Palu, Indonesia (2018)\",\n    lat: -0.26,\n    lon: 119.85,\n    magnitude: 7.5,\n    boundary: \"Transform\",\n  },\n  {\n    name: \"Ridgecrest, USA (2019)\",\n    lat: 35.77,\n    lon: -117.6,\n    magnitude: 7.1,\n    boundary: \"Transform\",\n  },\n  {\n    name: \"Kahramanmaraş, Turkey (2023)\",\n    lat: 37.17,\n    lon: 37.03,\n    magnitude: 7.8,\n    boundary: \"Transform\",\n  },\n  {\n    name: \"Al Haouz, Morocco (2023)\",\n    lat: 31.06,\n    lon: -8.39,\n    magnitude: 6.8,\n    boundary: \"Convergent\",\n  },\n  {\n    name: \"Reykjanes, Iceland (2021)\",\n    lat: 63.9,\n    lon: -22.27,\n    magnitude: 5.0,\n    boundary: \"Divergent\",\n  },\n  {\n    name: \"Lake Kivu, DR Congo (2008)\",\n    lat: -1.6,\n    lon: 29.2,\n    magnitude: 6.0,\n    boundary: \"Divergent\",\n  },\n  {\n    name: \"Azores, Portugal (1998)\",\n    lat: 38.5,\n    lon: -28.5,\n    magnitude: 6.1,\n    boundary: \"Divergent\",\n  },\n  {\n    name: \"El Mayor-Cucapah, Mexico (2010)\",\n    lat: 32.29,\n    lon: -115.3,\n    magnitude: 7.2,\n    boundary: \"Transform\",\n  },\n  {\n    name: \"Kobe, Japan (1995)\",\n    lat: 34.69,\n    lon: 135.2,\n    magnitude: 6.9,\n    boundary: \"Convergent\",\n  },\n  {\n    name: \"Northridge, USA (1994)\",\n    lat: 34.21,\n    lon: -118.54,\n    magnitude: 6.7,\n    boundary: \"Transform\",\n  },\n  {\n    name: \"Izmit, Turkey (1999)\",\n    lat: 40.7,\n    lon: 29.91,\n    magnitude: 7.6,\n    boundary: \"Transform\",\n  },\n  {\n    name: \"Bam, Iran (2003)\",\n    lat: 29.11,\n    lon: 58.36,\n    magnitude: 6.6,\n    boundary: \"Convergent\",\n  },\n  {\n    name: \"Pisco, Peru (2007)\",\n    lat: -13.39,\n    lon: -76.6,\n    magnitude: 8.0,\n    boundary: \"Convergent\",\n  },\n  {\n    name: \"Samoa Islands (2009)\",\n    lat: -15.49,\n    lon: -172.1,\n    magnitude: 8.1,\n    boundary: \"Convergent\",\n  },\n  {\n    name: \"Van, Turkey (2011)\",\n    lat: 38.75,\n    lon: 43.44,\n    magnitude: 7.1,\n    boundary: \"Transform\",\n  },\n  {\n    name: \"Papua New Guinea (2018)\",\n    lat: -6.06,\n    lon: 143.42,\n    magnitude: 7.5,\n    boundary: \"Convergent\",\n  },\n  {\n    name: \"Fiji (2018)\",\n    lat: -18.11,\n    lon: 178.16,\n    magnitude: 8.2,\n    boundary: \"Convergent\",\n  },\n  {\n    name: \"Alboran Sea, Spain (2016)\",\n    lat: 35.62,\n    lon: -3.69,\n    magnitude: 6.4,\n    boundary: \"Convergent\",\n  },\n];\n\nconst BOUNDARY_ORDER = [\"Convergent\", \"Transform\", \"Divergent\"];\nconst BOUNDARY_COLORS = {\n  Convergent: t.palette[0],\n  Transform: t.palette[1],\n  Divergent: t.palette[2],\n};\n\n// --- Deterministic render-only jitter for the dense California/Baja --------\n// --- Transform cluster, which otherwise reads as a single blob at map ------\n// --- scale; underlying quake.lat/lon stay the true historical values -------\nconst RENDER_JITTER = {\n  \"San Francisco, USA (1906)\": { dLon: -1.6, dLat: 1.3 },\n  \"Ridgecrest, USA (2019)\": { dLon: 1.6, dLat: 0.6 },\n  \"Northridge, USA (1994)\": { dLon: -1.6, dLat: -0.9 },\n  \"El Mayor-Cucapah, Mexico (2010)\": { dLon: 1.6, dLat: -1.5 },\n};\n\n// --- Bubble sizing: area (not radius) scales with magnitude -----------------\nconst MIN_R = 6;\nconst MAX_R = 30;\nconst magnitudes = earthquakes.map((quake) => quake.magnitude);\nconst magMin = Math.min(...magnitudes);\nconst magMax = Math.max(...magnitudes);\n\nfunction radiusFor(magnitude) {\n  const normalized = (magnitude - magMin) / (magMax - magMin);\n  return MIN_R + (MAX_R - MIN_R) * Math.sqrt(normalized);\n}\n\nfunction hexToRgba(hex, alpha) {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\nconst series = BOUNDARY_ORDER.map((boundary) => ({\n  name: `${boundary} boundary`,\n  color: BOUNDARY_COLORS[boundary],\n  zIndex: 5,\n  data: earthquakes\n    .filter((quake) => quake.boundary === boundary)\n    .map((quake) => {\n      const jitter = RENDER_JITTER[quake.name];\n      return {\n        x: quake.lon + (jitter ? jitter.dLon : 0),\n        y: quake.lat + (jitter ? jitter.dLat : 0),\n        name: quake.name,\n        magnitude: quake.magnitude,\n        marker: {\n          radius: radiusFor(quake.magnitude),\n          fillColor: hexToRgba(BOUNDARY_COLORS[boundary], 0.62),\n          lineColor: t.pageBg,\n          lineWidth: 1.5,\n        },\n      };\n    }),\n}));\n\n// --- Simplified world coastlines (equirectangular lon/lat vertices) ---------\nconst CONTINENTS = [\n  [\n    [-170, 60],\n    [-160, 58],\n    [-145, 60],\n    [-130, 55],\n    [-125, 48],\n    [-124, 40],\n    [-117, 32],\n    [-110, 31],\n    [-105, 28],\n    [-97, 26],\n    [-90, 29],\n    [-82, 24],\n    [-80, 26],\n    [-75, 35],\n    [-67, 44],\n    [-60, 46],\n    [-65, 50],\n    [-75, 50],\n    [-80, 52],\n    [-95, 52],\n    [-110, 52],\n    [-125, 55],\n    [-135, 58],\n    [-150, 58],\n    [-170, 60],\n  ],\n  [\n    [-81, 9],\n    [-77, 4],\n    [-70, -2],\n    [-59, -3],\n    [-50, -1],\n    [-44, -3],\n    [-35, -8],\n    [-35, -15],\n    [-40, -23],\n    [-48, -26],\n    [-53, -33],\n    [-58, -38],\n    [-68, -55],\n    [-72, -50],\n    [-75, -42],\n    [-71, -30],\n    [-70, -20],\n    [-78, -6],\n    [-81, 9],\n  ],\n  [\n    [-9, 43],\n    [-5, 44],\n    [2, 42],\n    [3, 44],\n    [7, 44],\n    [10, 46],\n    [13, 45],\n    [13, 41],\n    [16, 40],\n    [20, 40],\n    [24, 36],\n    [27, 39],\n    [29, 41],\n    [35, 42],\n    [41, 42],\n    [45, 42],\n    [41, 47],\n    [35, 49],\n    [30, 50],\n    [24, 54],\n    [19, 54],\n    [14, 54],\n    [9, 54],\n    [5, 52],\n    [3, 51],\n    [1, 51],\n    [-3, 50],\n    [-6, 48],\n    [-9, 43],\n  ],\n  [\n    [-6, 50],\n    [-5, 54],\n    [-4, 58],\n    [-8, 58],\n    [-6, 55],\n    [-6, 50],\n  ],\n  [\n    [-17, 15],\n    [-16, 21],\n    [-9, 32],\n    [-1, 36],\n    [10, 37],\n    [20, 32],\n    [30, 31],\n    [33, 30],\n    [36, 27],\n    [43, 12],\n    [51, 12],\n    [51, 2],\n    [42, -4],\n    [40, -15],\n    [35, -22],\n    [32, -27],\n    [27, -33],\n    [20, -35],\n    [14, -23],\n    [12, -6],\n    [9, 4],\n    [2, 6],\n    [-5, 5],\n    [-11, 7],\n    [-17, 15],\n  ],\n  [\n    [27, 68],\n    [45, 68],\n    [60, 68],\n    [75, 72],\n    [95, 75],\n    [120, 75],\n    [140, 72],\n    [160, 65],\n    [170, 62],\n    [165, 55],\n    [150, 46],\n    [140, 44],\n    [135, 35],\n    [128, 35],\n    [122, 32],\n    [110, 22],\n    [104, 10],\n    [100, 6],\n    [96, 6],\n    [92, 22],\n    [88, 22],\n    [80, 20],\n    [76, 10],\n    [70, 25],\n    [60, 25],\n    [50, 30],\n    [43, 12],\n    [36, 27],\n    [33, 30],\n    [30, 31],\n    [27, 68],\n  ],\n  [\n    [130, 32],\n    [132, 34],\n    [135, 35],\n    [139, 36],\n    [141, 40],\n    [141, 45],\n    [144, 44],\n    [144, 38],\n    [140, 36],\n    [135, 35],\n    [130, 32],\n  ],\n  [\n    [113, -22],\n    [122, -18],\n    [130, -12],\n    [137, -16],\n    [141, -13],\n    [145, -16],\n    [150, -23],\n    [153, -28],\n    [150, -38],\n    [141, -38],\n    [130, -32],\n    [117, -35],\n    [113, -25],\n    [113, -22],\n  ],\n];\n\n// --- Corner-cutting refinement (2 Chaikin iterations) so the coastlines -----\n// --- read as smooth curves instead of the raw low-vertex source polygons ---\nfunction smoothPolygon(points, iterations) {\n  let pts = points;\n  for (let iter = 0; iter < iterations; iter++) {\n    const refined = [];\n    for (let i = 0; i < pts.length - 1; i++) {\n      const [x0, y0] = pts[i];\n      const [x1, y1] = pts[i + 1];\n      refined.push([x0 + 0.25 * (x1 - x0), y0 + 0.25 * (y1 - y0)]);\n      refined.push([x0 + 0.75 * (x1 - x0), y0 + 0.75 * (y1 - y0)]);\n    }\n    refined.push(refined[0]);\n    pts = refined;\n  }\n  return pts;\n}\n\nfunction drawContinents(chart) {\n  const xAxis = chart.xAxis[0];\n  const yAxis = chart.yAxis[0];\n  CONTINENTS.forEach((polygon) => {\n    const smoothed = smoothPolygon(polygon, 2);\n    const path = smoothed.map((point, i) => [\n      i === 0 ? \"M\" : \"L\",\n      xAxis.toPixels(point[0], false),\n      yAxis.toPixels(point[1], false),\n    ]);\n    path.push([\"Z\"]);\n    chart.renderer\n      .path(path)\n      .attr({ fill: LAND, stroke: LAND_EDGE, \"stroke-width\": 1, zIndex: 1 })\n      .add();\n  });\n}\n\n// --- Size legend (core Highcharts has no bubble legend — drawn manually) ----\n// Uses its own compact radius scale (independent of the map's marker radii)\n// so three reference circles fit comfortably inside the legend box.\nconst LEGEND_MIN_R = 8;\nconst LEGEND_MAX_R = 24;\nfunction legendRadiusFor(magnitude) {\n  const normalized = (magnitude - magMin) / (magMax - magMin);\n  return LEGEND_MIN_R + (LEGEND_MAX_R - LEGEND_MIN_R) * Math.sqrt(normalized);\n}\n\nfunction drawSizeLegend(chart) {\n  // Offset clears the Samoa Islands bubble, which sits near the antimeridian.\n  const boxX = chart.plotLeft + 170;\n  const boxW = 190;\n  const boxH = 220;\n  const boxY = chart.plotTop + chart.plotHeight - boxH - 24;\n\n  chart.renderer\n    .rect(boxX, boxY, boxW, boxH, 6)\n    .attr({\n      fill: t.elevatedBg,\n      stroke: t.inkSoft,\n      \"stroke-width\": 1,\n      zIndex: 6,\n      opacity: 0.94,\n    })\n    .add();\n\n  chart.renderer\n    .text(\"Magnitude\", boxX + boxW / 2, boxY + 26)\n    .attr({ align: \"center\", zIndex: 7 })\n    .css({ color: t.ink, fontSize: \"14px\", fontWeight: \"600\" })\n    .add();\n\n  let cursorY = boxY + 48;\n  [9.0, 7.5, 6.0].forEach((magnitude) => {\n    const r = legendRadiusFor(magnitude);\n    const cy = cursorY + r;\n    chart.renderer\n      .circle(boxX + 46, cy, r)\n      .attr({\n        fill: t.inkSoft,\n        opacity: 0.55,\n        stroke: t.pageBg,\n        \"stroke-width\": 1,\n        zIndex: 7,\n      })\n      .add();\n    chart.renderer\n      .text(`M${magnitude.toFixed(1)}`, boxX + 46 + LEGEND_MAX_R + 16, cy + 5)\n      .attr({ zIndex: 7 })\n      .css({ color: t.inkSoft, fontSize: \"13px\" })\n      .add();\n    cursorY += 2 * r + 14;\n  });\n}\n\n// --- Chart --------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\",\n    backgroundColor: \"transparent\",\n    plotBackgroundColor: OCEAN,\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    events: {\n      load: function () {\n        drawContinents(this);\n        drawSizeLegend(this);\n      },\n    },\n  },\n  credits: { enabled: false },\n  title: {\n    text: \"bubble-map-geographic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Significant earthquakes, sized by magnitude and colored by tectonic boundary type — note the convergent-boundary clustering around the Pacific “Ring of Fire”\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    min: -180,\n    max: 180,\n    tickInterval: 30,\n    title: {\n      text: \"Longitude (°)\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineWidth: 1,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  yAxis: {\n    min: -60,\n    max: 85,\n    tickInterval: 30,\n    title: {\n      text: \"Latitude (°)\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineWidth: 1,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  tooltip: {\n    headerFormat: \"\",\n    pointFormatter: function () {\n      return `<b>${this.name}</b><br/>Magnitude M${this.magnitude.toFixed(1)}<br/>${this.series.name}`;\n    },\n  },\n  plotOptions: {\n    series: {\n      animation: false,\n      marker: { symbol: \"circle\", states: { hover: { enabled: false } } },\n    },\n  },\n  series,\n});\n"}