{"spec_id":"scatter-map-geographic","library":"muix","language":"javascript","code":"// anyplot.ai\n// scatter-map-geographic: Scatter Map with Geographic Points\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-02\nimport { ScatterChart } from \"@mui/x-charts/ScatterChart\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport { ContinuousColorLegend } from \"@mui/x-charts/ChartsLegend\";\nimport { Box, Typography } from \"@mui/material\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Simulated M5.0+ earthquake epicenters clustered along the world's major\n// subduction and collision zones (Ring of Fire, Alpide belt). Longitude/\n// latitude place each point on an equirectangular (plate carree) projection —\n// the simplest valid map projection: lon maps linearly to x, lat to y. A\n// 60 deg/30 deg graticule plus the equator and prime meridian reference lines\n// stand in for a basemap (no coastline dataset ships with the community\n// package and pulling one in would mean importing D3, which is out of scope\n// for muix).\nlet seed = 8831;\nconst rand = () => {\n  seed = (seed * 1103515245 + 12345) % 2147483648;\n  return seed / 2147483648;\n};\n\nconst FAULT_ZONES = [\n  { lon: 142, lat: 38, spreadLon: 6, spreadLat: 6, count: 12, mag: [5.0, 7.2], depth: [10, 300] }, // Japan-Kuril arc\n  { lon: -160, lat: 55, spreadLon: 10, spreadLat: 4, count: 8, mag: [5.0, 7.5], depth: [10, 150] }, // Aleutian-Alaska arc\n  { lon: -112, lat: 24, spreadLon: 9, spreadLat: 15, count: 10, mag: [5.0, 6.9], depth: [5, 60] }, // Cascadia-Mexico coast\n  { lon: -72, lat: -20, spreadLon: 3, spreadLat: 16, count: 12, mag: [5.0, 7.8], depth: [10, 600] }, // Andes subduction\n  { lon: 115, lat: 2, spreadLon: 18, spreadLat: 10, count: 14, mag: [5.0, 7.6], depth: [10, 400] }, // Indonesia-Philippines arc\n  { lon: 178, lat: -20, spreadLon: 4, spreadLat: 8, count: 8, mag: [5.0, 7.4], depth: [10, 600] }, // Tonga-Kermadec trench\n  { lon: 30, lat: 37, spreadLon: 12, spreadLat: 5, count: 8, mag: [5.0, 6.8], depth: [5, 40] }, // Anatolia-Aegean\n  { lon: 84, lat: 29, spreadLon: 10, spreadLat: 4, count: 8, mag: [5.0, 7.2], depth: [5, 40] }, // Himalayan front\n];\n\nconst epicenters = FAULT_ZONES.flatMap((zone, zoneIndex) =>\n  Array.from({ length: zone.count }, (_, i) => {\n    const longitude = Math.max(-180, Math.min(180, zone.lon + (rand() - 0.5) * 2 * zone.spreadLon));\n    const latitude = Math.max(-90, Math.min(90, zone.lat + (rand() - 0.5) * 2 * zone.spreadLat));\n    const magnitude = zone.mag[0] + rand() * (zone.mag[1] - zone.mag[0]);\n    const depthKm = Math.round(zone.depth[0] + rand() * (zone.depth[1] - zone.depth[0]));\n    return { id: `eq-${zoneIndex}-${i}`, longitude, latitude, magnitude, depthKm };\n  }),\n);\n\nconst depths = epicenters.map((e) => e.depthKm);\nconst depthDomain = [Math.min(...depths), Math.max(...depths)];\n\n// Magnitude tiers -> marker size. The community ScatterChart sizes markers\n// per series (not per point), so a size legend for a continuous magnitude\n// needs binning into a handful of series, each drawn at its own markerSize.\nconst MAGNITUDE_TIERS = [\n  { id: \"minor\", label: \"M 5.0-5.6\", max: 5.6, markerSize: 5 },\n  { id: \"light\", label: \"M 5.6-6.3\", max: 6.3, markerSize: 8 },\n  { id: \"strong\", label: \"M 6.3-7.0\", max: 7.0, markerSize: 12 },\n  { id: \"major\", label: \"M 7.0+\", max: Infinity, markerSize: 17 },\n];\n\nconst tierSeries = MAGNITUDE_TIERS.map((tier, tierIndex) => {\n  const lowerBound = tierIndex === 0 ? -Infinity : MAGNITUDE_TIERS[tierIndex - 1].max;\n  const points = epicenters.filter((e) => e.magnitude > lowerBound && e.magnitude <= tier.max);\n  return {\n    type: \"scatter\",\n    id: tier.id,\n    label: tier.label,\n    markerSize: tier.markerSize,\n    color: t.palette[0],\n    zAxisId: \"depth\",\n    data: points.map((e) => ({ x: e.longitude, y: e.latitude, z: e.depthKm, id: e.id })),\n    valueFormatter: (v) => `${v.x.toFixed(1)}°, ${v.y.toFixed(1)}° · depth ${v.z} km`,\n  };\n});\n\nconst formatLongitude = (value) => (value === 0 ? \"0°\" : `${Math.abs(Math.round(value))}°${value > 0 ? \"E\" : \"W\"}`);\nconst formatLatitude = (value) => (value === 0 ? \"0°\" : `${Math.abs(Math.round(value))}°${value > 0 ? \"N\" : \"S\"}`);\n\nconst TITLE = \"scatter-map-geographic · javascript · muix · anyplot.ai\";\nconst SUBTITLE = \"Marker size = magnitude tier · marker color = depth (see scale, top right)\";\n\n// Minimal hardcoded [lon, lat] continent outlines (plate-carree, heavily\n// simplified) — a lightweight basemap so the plot reads as a world map at a\n// glance without pulling in a geo/D3 dependency, which is out of scope for\n// the community-only muix surface.\nconst CONTINENTS = [\n  {\n    name: \"north-america\",\n    points: [\n      [-165, 68], [-150, 71], [-130, 70], [-95, 73], [-80, 68], [-65, 60], [-55, 52],\n      [-65, 46], [-75, 36], [-81, 25], [-97, 19], [-105, 21], [-114, 31], [-124, 40],\n      [-124, 49], [-130, 55], [-140, 60], [-155, 60], [-165, 68],\n    ],\n  },\n  {\n    name: \"south-america\",\n    points: [\n      [-79, 9], [-77, 1], [-70, -4], [-70, -18], [-72, -30], [-70, -40], [-68, -52],\n      [-65, -55], [-58, -51], [-53, -35], [-48, -24], [-40, -10], [-35, -2], [-50, 5],\n      [-60, 9], [-70, 11], [-79, 9],\n    ],\n  },\n  {\n    name: \"africa\",\n    points: [\n      [-17, 15], [-16, 7], [-9, 5], [2, 6], [9, 4], [9, -5], [12, -18], [16, -28],\n      [20, -34], [27, -33], [33, -26], [35, -20], [40, -15], [40, -3], [43, -1],\n      [51, 10], [45, 12], [38, 15], [32, 20], [25, 30], [15, 32], [9, 36], [0, 35],\n      [-8, 32], [-17, 15],\n    ],\n  },\n  {\n    name: \"eurasia\",\n    points: [\n      [-10, 37], [-9, 43], [-5, 48], [3, 51], [10, 54], [15, 55], [20, 54], [25, 60],\n      [30, 63], [35, 66], [45, 68], [60, 70], [75, 73], [95, 75], [115, 73], [135, 70],\n      [142, 60], [135, 50], [142, 45], [140, 36], [130, 32], [122, 31], [110, 20],\n      [100, 10], [92, 5], [80, 8], [70, 20], [60, 25], [50, 30], [42, 37], [35, 35],\n      [28, 42], [20, 40], [15, 38], [8, 36], [-10, 37],\n    ],\n  },\n  {\n    name: \"australia\",\n    points: [\n      [113, -22], [120, -17], [130, -12], [137, -12], [145, -16], [150, -24],\n      [153, -28], [153, -32], [147, -38], [140, -38], [130, -32], [120, -34],\n      [114, -30], [113, -22],\n    ],\n  },\n];\n\n// Direct plate-carree projection matching the chart's linear lon/lat axes\n// (min/max -180..180 / -90..90, no scale padding) so the outlines line up\n// exactly with the data points and graticule.\nconst projectPath = (points, w, h) =>\n  points\n    .map(([lon, lat], i) => `${i === 0 ? \"M\" : \"L\"} ${((lon + 180) / 360) * w} ${((90 - lat) / 180) * h}`)\n    .join(\" \") + \" Z\";\n\n// --- Chart (default-exported component — the harness mounts it) ------------\nexport default function Chart() {\n  const size = window.ANYPLOT_SIZE;\n  const padding = { top: 28, right: 40, bottom: 24, left: 40 };\n  const titleBlockHeight = 76;\n  const chartWidth = size.width - padding.left - padding.right;\n  const chartHeight = size.height - padding.top - padding.bottom - titleBlockHeight;\n  const margin = { top: 76, right: 30, bottom: 100, left: 84 };\n  const drawWidth = chartWidth - margin.left - margin.right;\n  const drawHeight = chartHeight - margin.top - margin.bottom;\n\n  return (\n    <Box\n      sx={{\n        width: size.width,\n        height: size.height,\n        boxSizing: \"border-box\",\n        padding: `${padding.top}px ${padding.right}px ${padding.bottom}px ${padding.left}px`,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      <Typography sx={{ fontSize: 24, fontWeight: 600, color: \"text.primary\", mb: \"6px\", lineHeight: 1 }}>\n        {TITLE}\n      </Typography>\n      <Typography sx={{ fontSize: 13, color: \"text.secondary\", mb: \"16px\", lineHeight: 1 }}>{SUBTITLE}</Typography>\n      <Box sx={{ position: \"relative\", width: chartWidth, height: chartHeight }}>\n        <Box\n          sx={{\n            position: \"absolute\",\n            left: margin.left,\n            top: margin.top,\n            width: drawWidth,\n            height: drawHeight,\n            overflow: \"hidden\",\n            pointerEvents: \"none\",\n          }}\n        >\n          <svg width={drawWidth} height={drawHeight} style={{ display: \"block\" }}>\n            {CONTINENTS.map((c) => (\n              <path\n                key={c.name}\n                d={projectPath(c.points, drawWidth, drawHeight)}\n                fill={t.grid}\n                stroke={t.inkSoft}\n                strokeOpacity={0.45}\n                strokeWidth={1}\n              />\n            ))}\n          </svg>\n        </Box>\n        <ScatterChart\n          width={chartWidth}\n          height={chartHeight}\n          skipAnimation\n          disableVoronoi\n          series={tierSeries}\n          margin={margin}\n          xAxis={[\n            {\n              id: \"lon\",\n              min: -180,\n              max: 180,\n              domainLimit: \"strict\",\n              label: \"Longitude\",\n              disableLine: true,\n              tickInterval: [-180, -120, -60, 0, 60, 120, 180],\n              valueFormatter: (v, ctx) => (ctx.location === \"tick\" ? formatLongitude(v) : `${v.toFixed(1)}°`),\n              tickLabelStyle: { fontSize: 14 },\n              labelStyle: { fontSize: 16 },\n            },\n          ]}\n          yAxis={[\n            {\n              id: \"lat\",\n              min: -90,\n              max: 90,\n              domainLimit: \"strict\",\n              label: \"Latitude\",\n              disableLine: true,\n              tickInterval: [-90, -60, -30, 0, 30, 60, 90],\n              valueFormatter: (v, ctx) => (ctx.location === \"tick\" ? formatLatitude(v) : `${v.toFixed(1)}°`),\n              tickLabelStyle: { fontSize: 14 },\n              labelStyle: { fontSize: 16 },\n            },\n          ]}\n          zAxis={[\n            {\n              id: \"depth\",\n              min: depthDomain[0],\n              max: depthDomain[1],\n              colorMap: { type: \"continuous\", min: depthDomain[0], max: depthDomain[1], color: [t.seq[0], t.seq[1]] },\n            },\n          ]}\n          grid={{ horizontal: true, vertical: true }}\n          slotProps={{\n            legend: {\n              direction: \"row\",\n              position: { vertical: \"bottom\", horizontal: \"middle\" },\n              labelStyle: { fontSize: 14 },\n              itemMarkWidth: 16,\n              itemMarkHeight: 10,\n              markGap: 6,\n              itemGap: 20,\n            },\n          }}\n          sx={{\n            \"& circle\": { fillOpacity: 0.82, stroke: t.pageBg, strokeWidth: 1 },\n            \"& .MuiChartsGrid-line\": { stroke: t.grid, strokeDasharray: \"none\" },\n          }}\n        >\n          <ChartsReferenceLine\n            x={0}\n            label=\"Prime Meridian\"\n            labelAlign=\"end\"\n            lineStyle={{ stroke: t.inkSoft, strokeDasharray: \"6 4\", strokeWidth: 1.5 }}\n            labelStyle={{ fontSize: 12, fill: t.inkSoft }}\n          />\n          <ChartsReferenceLine\n            y={0}\n            label=\"Equator\"\n            labelAlign=\"end\"\n            lineStyle={{ stroke: t.inkSoft, strokeDasharray: \"6 4\", strokeWidth: 1.5 }}\n            labelStyle={{ fontSize: 12, fill: t.inkSoft }}\n          />\n          <ContinuousColorLegend\n            axisDirection=\"z\"\n            axisId=\"depth\"\n            direction=\"row\"\n            position={{ horizontal: \"right\", vertical: \"top\" }}\n            length={180}\n            thickness={12}\n            minLabel={({ formattedValue }) => `${formattedValue} km shallow`}\n            maxLabel={({ formattedValue }) => `${formattedValue} km deep`}\n            labelStyle={{ fontSize: 13, fill: t.inkSoft }}\n          />\n        </ScatterChart>\n      </Box>\n    </Box>\n  );\n}\n"}