{"spec_id":"scatter-map-geographic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// scatter-map-geographic: Scatter Map with Geographic Points\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-02\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Notable earthquake epicenters (illustrative, approximate). Magnitude drives\n// point size, focal depth drives point color (continuous, imprint_seq).\nconst RAW_EARTHQUAKES = [\n  { place: \"Tohoku, Japan\", lon: 142.37, lat: 38.3, mag: 7.8, depth: 32 },\n  { place: \"Sumatra, Indonesia\", lon: 95.85, lat: 3.3, mag: 8.1, depth: 22 },\n  { place: \"Sulawesi, Indonesia\", lon: 119.85, lat: -0.18, mag: 6.5, depth: 15 },\n  { place: \"Mindanao, Philippines\", lon: 126.6, lat: 6.6, mag: 6.9, depth: 45 },\n  { place: \"Kanto, Japan\", lon: 139.7, lat: 35.7, mag: 5.4, depth: 60 },\n  { place: \"Kamchatka, Russia\", lon: 160.0, lat: 53.0, mag: 7.2, depth: 180 },\n  { place: \"Aleutian Islands, USA\", lon: -176.0, lat: 51.9, mag: 6.8, depth: 35 },\n  { place: \"Southern Alaska, USA\", lon: -149.9, lat: 61.2, mag: 7.1, depth: 47 },\n  { place: \"Cascadia, USA\", lon: -124.0, lat: 44.0, mag: 5.9, depth: 20 },\n  { place: \"Baja California, Mexico\", lon: -115.3, lat: 32.3, mag: 6.4, depth: 10 },\n  { place: \"Michoacan, Mexico\", lon: -102.5, lat: 18.4, mag: 7.4, depth: 24 },\n  { place: \"Guatemala\", lon: -90.5, lat: 14.6, mag: 6.2, depth: 65 },\n  { place: \"Managua, Nicaragua\", lon: -86.2, lat: 12.1, mag: 5.8, depth: 40 },\n  { place: \"Valparaiso, Chile\", lon: -71.6, lat: -33.0, mag: 8.2, depth: 28 },\n  { place: \"Concepcion, Chile\", lon: -73.0, lat: -36.8, mag: 8.8, depth: 35 },\n  { place: \"Arequipa, Peru\", lon: -71.5, lat: -16.4, mag: 7.0, depth: 55 },\n  { place: \"Bogota, Colombia\", lon: -74.1, lat: 4.6, mag: 5.5, depth: 150 },\n  { place: \"Ecuador Coast\", lon: -80.0, lat: -1.0, mag: 6.7, depth: 20 },\n  { place: \"Reykjanes, Iceland\", lon: -22.5, lat: 63.9, mag: 5.2, depth: 8 },\n  { place: \"Azores\", lon: -25.7, lat: 37.7, mag: 5.6, depth: 12 },\n  { place: \"L'Aquila, Italy\", lon: 13.4, lat: 42.35, mag: 6.3, depth: 9 },\n  { place: \"Izmir, Turkey\", lon: 27.1, lat: 38.4, mag: 6.9, depth: 17 },\n  { place: \"Athens, Greece\", lon: 23.7, lat: 37.98, mag: 5.9, depth: 14 },\n  { place: \"Tehran, Iran\", lon: 51.4, lat: 35.7, mag: 6.1, depth: 25 },\n  { place: \"Kashmir, Pakistan\", lon: 73.5, lat: 34.5, mag: 7.6, depth: 26 },\n  { place: \"Gorkha, Nepal\", lon: 84.7, lat: 28.2, mag: 7.8, depth: 15 },\n  { place: \"Assam, India\", lon: 92.9, lat: 26.2, mag: 6.4, depth: 34 },\n  { place: \"Sichuan, China\", lon: 103.4, lat: 31.0, mag: 7.9, depth: 19 },\n  { place: \"Taiwan Strait\", lon: 121.5, lat: 23.9, mag: 6.6, depth: 12 },\n  { place: \"Ryukyu Islands, Japan\", lon: 128.2, lat: 26.5, mag: 6.0, depth: 130 },\n  { place: \"Vanuatu\", lon: 167.8, lat: -17.7, mag: 7.3, depth: 42 },\n  { place: \"Tonga Trench\", lon: -173.0, lat: -21.1, mag: 7.9, depth: 205 },\n  { place: \"Fiji\", lon: 178.0, lat: -18.0, mag: 6.5, depth: 400 },\n  { place: \"North Island, New Zealand\", lon: 176.2, lat: -38.7, mag: 6.3, depth: 30 },\n  { place: \"South Island, New Zealand\", lon: 172.6, lat: -43.5, mag: 7.1, depth: 15 },\n  { place: \"Papua New Guinea\", lon: 147.2, lat: -6.7, mag: 7.0, depth: 90 },\n  { place: \"East Africa Rift, Tanzania\", lon: 35.6, lat: -6.2, mag: 5.7, depth: 18 },\n  { place: \"Algiers, Algeria\", lon: 3.0, lat: 36.7, mag: 6.0, depth: 10 },\n  { place: \"Cape Town Margin, S. Africa\", lon: 20.0, lat: -33.0, mag: 4.8, depth: 8 },\n  { place: \"Sea of Okhotsk\", lon: 148.0, lat: 54.0, mag: 6.8, depth: 580 },\n  { place: \"Bonin Islands, Japan\", lon: 140.5, lat: 27.0, mag: 6.2, depth: 470 },\n];\n\n// --- Basemap: simplified continent silhouettes (rough, low-vertex trace --\n// geographic context only, not a precise survey boundary). Rendered as\n// unprojected (equirectangular / plate carrée) lon/lat on the chart's own\n// value axes rather than a Natural Earth/Robinson or Mercator projection:\n// ECharts has no bundled world GeoJSON to register, and reprojecting the\n// data points to match a curved projection without fetching one would need\n// an extra library. Equirectangular is an accepted world-map tradeoff. -----\nconst NORTH_AMERICA = [\n  [-168, 66], [-155, 71], [-140, 70], [-125, 55], [-124, 46], [-120, 34],\n  [-110, 23], [-105, 20], [-97, 16], [-88, 14], [-83, 9], [-79, 8],\n  [-81, 25], [-76, 35], [-70, 41], [-66, 44], [-60, 47], [-55, 51],\n  [-65, 60], [-80, 63], [-100, 70], [-120, 72], [-140, 70], [-155, 71], [-168, 66],\n];\nconst SOUTH_AMERICA = [\n  [-79, 9], [-77, 1], [-80, -4], [-81, -14], [-75, -19], [-70, -30],\n  [-71, -40], [-73, -50], [-68, -55], [-64, -52], [-58, -38], [-48, -25],\n  [-35, -8], [-48, 3], [-60, 6], [-72, 8], [-79, 9],\n];\nconst AFRICA = [\n  [-17, 15], [-16, 21], [-9, 32], [0, 37], [10, 37], [20, 32], [32, 31],\n  [35, 27], [43, 13], [51, 12], [45, 0], [40, -12], [35, -20], [33, -27],\n  [27, -33], [18, -35], [13, -23], [12, -6], [9, 4], [-5, 5], [-11, 7], [-17, 15],\n];\nconst EURASIA = [\n  [-9, 37], [-9, 44], [-5, 48], [3, 51], [8, 58], [25, 70], [40, 68],\n  [60, 72], [90, 76], [130, 76], [170, 67], [160, 55], [142, 45], [130, 35],\n  [122, 31], [108, 10], [100, 6], [95, 16], [88, 22], [80, 9], [73, 20],\n  [65, 25], [55, 26], [48, 14], [38, 10], [35, 15], [35, 32], [27, 36],\n  [23, 37], [12, 38], [-5, 36], [-9, 37],\n];\nconst AUSTRALIA = [\n  [113, -22], [114, -30], [121, -34], [130, -32], [137, -35], [141, -38],\n  [147, -38], [150, -37], [153, -28], [145, -17], [142, -11], [136, -12],\n  [130, -12], [124, -15], [113, -22],\n];\nconst CONTINENTS = [NORTH_AMERICA, SOUTH_AMERICA, AFRICA, EURASIA, AUSTRALIA];\n\n// Fills+outlines one continent polygon; coordinates are projected through the\n// chart's own lon/lat value axes via api.coord (cartesian2d custom series --\n// ECharts has no bundled world GeoJSON to register, so the basemap is drawn\n// as ordinary shapes on the same axes the data points use).\nfunction makeLandRenderer(coords) {\n  return function renderLand(params, api) {\n    const points = coords.map(([lon, lat]) => api.coord([lon, lat]));\n    return {\n      type: \"polygon\",\n      shape: { points },\n      style: { fill: t.grid, stroke: t.inkSoft, lineWidth: 1, opacity: 0.5 },\n      silent: true,\n    };\n  };\n}\n\nconst MAGS = RAW_EARTHQUAKES.map((e) => e.mag);\nconst DEPTHS = RAW_EARTHQUAKES.map((e) => e.depth);\nconst MAG_MIN = Math.min(...MAGS);\nconst MAG_MAX = Math.max(...MAGS);\nconst DEPTH_MIN = Math.min(...DEPTHS);\nconst DEPTH_MAX = Math.max(...DEPTHS);\nconst SIZE_MIN = 14;\nconst SIZE_MAX = 46;\n\n// Magnitude is already a log-scaled quantity (each unit ~31x the energy), so\n// point diameter scales linearly with magnitude rather than sqrt(value).\nfunction magSize(mag) {\n  const ratio = (mag - MAG_MIN) / (MAG_MAX - MAG_MIN);\n  return SIZE_MIN + ratio * (SIZE_MAX - SIZE_MIN);\n}\n\n// --- Declutter: nudge apart markers whose rendered circles would overlap ----\n// (magnitude-driven radii can exceed the geographic spacing between nearby\n// epicenters, e.g. Valparaiso/Concepcion, Izmir/Athens). Positions are\n// relaxed in the chart's own pixel space, mirroring the axes/grid below, then\n// converted back to lon/lat; tooltip content (name/mag/depth) is unaffected.\nconst GRID = { left: 90, top: 140, width: 1300, height: 580 };\nconst LON_RANGE = [-180, 180];\nconst LAT_RANGE = [-60, 80];\nfunction toPx(lon, lat) {\n  return [\n    ((lon - LON_RANGE[0]) / (LON_RANGE[1] - LON_RANGE[0])) * GRID.width,\n    ((LAT_RANGE[1] - lat) / (LAT_RANGE[1] - LAT_RANGE[0])) * GRID.height,\n  ];\n}\nfunction fromPx(x, y) {\n  return [\n    LON_RANGE[0] + (x / GRID.width) * (LON_RANGE[1] - LON_RANGE[0]),\n    LAT_RANGE[1] - (y / GRID.height) * (LAT_RANGE[1] - LAT_RANGE[0]),\n  ];\n}\nfunction declutter(points, passes = 6) {\n  const px = points.map((p) => toPx(p.lon, p.lat));\n  for (let pass = 0; pass < passes; pass++) {\n    for (let i = 0; i < points.length; i++) {\n      for (let j = i + 1; j < points.length; j++) {\n        const dx = px[j][0] - px[i][0];\n        const dy = px[j][1] - px[i][1];\n        const dist = Math.hypot(dx, dy) || 0.01;\n        const minDist = magSize(points[i].mag) / 2 + magSize(points[j].mag) / 2 + 3;\n        if (dist < minDist) {\n          const push = (minDist - dist) / 2;\n          const ux = dx / dist;\n          const uy = dy / dist;\n          px[i][0] -= ux * push;\n          px[i][1] -= uy * push;\n          px[j][0] += ux * push;\n          px[j][1] += uy * push;\n        }\n      }\n    }\n  }\n  return points.map((p, i) => {\n    const [lon, lat] = fromPx(px[i][0], px[i][1]);\n    return { ...p, lon, lat };\n  });\n}\nconst EARTHQUAKES = declutter(RAW_EARTHQUAKES);\n\n// --- Init ---------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Magnitude size-key legend (fixed screen-space graphic) -----------------\nconst MAG_LEGEND_VALUES = [5, 6, 7, 8];\nconst LEGEND_X = 1500;\nconst legendGraphics = [\n  {\n    type: \"text\",\n    left: LEGEND_X - 34,\n    top: 470,\n    style: { text: \"Magnitude\", fill: t.inkSoft, fontSize: 13, fontWeight: 500 },\n  },\n];\nlet cy = 498;\nfor (const mag of MAG_LEGEND_VALUES) {\n  const r = magSize(mag) / 2;\n  cy += r;\n  legendGraphics.push({\n    type: \"circle\",\n    shape: { cx: LEGEND_X, cy, r },\n    style: { fill: \"none\", stroke: t.inkSoft, lineWidth: 1.5 },\n  });\n  legendGraphics.push({\n    type: \"text\",\n    left: LEGEND_X + r + 14,\n    top: cy - 7,\n    style: { text: `M${mag}`, fill: t.inkSoft, fontSize: 13 },\n  });\n  cy += r + 16;\n}\n\n// --- Option -----------------------------------------------------------------\nconst title = \"Global Earthquakes · scatter-map-geographic · javascript · echarts · anyplot.ai\";\n\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: title,\n    left: \"center\",\n    top: 24,\n    textStyle: { color: t.ink, fontSize: 19, fontWeight: 500 },\n  },\n  tooltip: {\n    trigger: \"item\",\n    formatter: (p) =>\n      `${p.data.name}<br/>M${p.data.value[3].toFixed(1)} · depth ${p.data.value[2]} km`,\n  },\n  visualMap: {\n    type: \"continuous\",\n    dimension: 2,\n    seriesIndex: CONTINENTS.length,\n    min: DEPTH_MIN,\n    max: DEPTH_MAX,\n    orient: \"vertical\",\n    right: 40,\n    top: 140,\n    itemHeight: 260,\n    calculable: false,\n    text: [`Deep (${DEPTH_MAX} km)`, `Shallow (${DEPTH_MIN} km)`],\n    textStyle: { color: t.inkSoft, fontSize: 13 },\n    inRange: { color: t.seq },\n  },\n  grid: GRID,\n  dataZoom: [\n    { type: \"inside\", xAxisIndex: 0, filterMode: \"none\" },\n    { type: \"inside\", yAxisIndex: 0, filterMode: \"none\" },\n  ],\n  xAxis: {\n    type: \"value\",\n    name: \"Longitude\",\n    nameLocation: \"middle\",\n    nameGap: 34,\n    nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n    min: LON_RANGE[0],\n    max: LON_RANGE[1],\n    interval: 30,\n    axisLabel: {\n      color: t.inkSoft,\n      fontSize: 12,\n      formatter: (v) => `${Math.abs(v)}°${v < 0 ? \"W\" : v > 0 ? \"E\" : \"\"}`,\n    },\n    axisLine: { onZero: false, lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"Latitude\",\n    nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n    min: LAT_RANGE[0],\n    max: LAT_RANGE[1],\n    interval: 30,\n    axisLabel: {\n      color: t.inkSoft,\n      fontSize: 12,\n      formatter: (v) => `${Math.abs(v)}°${v < 0 ? \"S\" : v > 0 ? \"N\" : \"\"}`,\n    },\n    axisLine: { onZero: false, lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  graphic: legendGraphics,\n  series: [\n    ...CONTINENTS.map((coords, i) => ({\n      name: `Basemap ${i}`,\n      type: \"custom\",\n      coordinateSystem: \"cartesian2d\",\n      renderItem: makeLandRenderer(coords),\n      data: [[0, 0]],\n      silent: true,\n      tooltip: { show: false },\n      z: 0,\n    })),\n    {\n      name: \"Earthquakes\",\n      type: \"scatter\",\n      data: EARTHQUAKES.map((e) => ({\n        name: e.place,\n        value: [e.lon, e.lat, e.depth, e.mag],\n      })),\n      symbolSize: (val) => magSize(val[3]),\n      itemStyle: { opacity: 0.75, borderColor: t.pageBg, borderWidth: 1 },\n      emphasis: { itemStyle: { opacity: 1 } },\n      z: 2,\n    },\n  ],\n});\n"}