{"spec_id":"map-projections","library":"echarts","language":"javascript","code":"// anyplot.ai\n// map-projections: World Map with Different Projections\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 81/100 | Created: 2026-08-26\n\nconst t = window.ANYPLOT_TOKENS;\nconst SIZE = window.ANYPLOT_SIZE;\n\n// --- Simplified landmass outlines (lon, lat degrees) ------------------------\n// Low-vertex silhouettes, not survey-accurate coastlines — enough to carry the\n// projection-distortion story (esp. Greenland/Antarctica vs. Africa in scale).\n// No offline per-country GeoJSON ships with the pinned echarts build, so land\n// is shown at continent granularity; a couple of illustrative (non-authoritative)\n// internal borders are layered on top to at least gesture at country boundaries.\nconst LANDMASS_NAMES = [\"Africa\", \"Eurasia\", \"North America\", \"South America\", \"Greenland\", \"Australia\", \"Antarctica\"];\nconst LANDMASSES = [\n  [\n    [-17, 15], [-16, 21], [-11, 24], [-6, 30], [10, 32], [20, 31], [32, 31],\n    [35, 20], [43, 12], [51, 12], [42, -2], [40, -15], [35, -25], [27, -34],\n    [18, -34], [12, -18], [9, 4], [0, 4], [-10, 7], [-17, 15],\n  ], // Africa\n  [\n    [-10, 36], [-9, 43], [-5, 48], [2, 51], [5, 58], [10, 60], [20, 60],\n    [28, 60], [30, 66], [40, 66], [45, 68], [60, 70], [75, 73], [100, 73],\n    [130, 72], [143, 60], [140, 45], [130, 35], [122, 31], [121, 25],\n    [110, 18], [103, 10], [95, 5], [80, 8], [68, 7], [60, 25], [50, 25],\n    [48, 30], [35, 32], [30, 36], [27, 36], [22, 40], [15, 40], [13, 45],\n    [-10, 36],\n  ], // Eurasia\n  [\n    [-165, 65], [-165, 70], [-140, 70], [-120, 75], [-95, 75], [-80, 73],\n    [-65, 68], [-60, 60], [-55, 52], [-65, 45], [-75, 35], [-80, 26],\n    [-97, 26], [-105, 20], [-115, 30], [-125, 40], [-125, 49], [-135, 58],\n    [-165, 65],\n  ], // North America\n  [\n    [-80, 10], [-77, 5], [-70, -5], [-70, -18], [-72, -30], [-70, -40],\n    [-68, -52], [-65, -55], [-58, -52], [-53, -35], [-40, -20], [-35, -8],\n    [-50, 2], [-60, 10], [-70, 12], [-80, 10],\n  ], // South America\n  [\n    [-55, 60], [-45, 60], [-30, 65], [-25, 70], [-20, 75], [-25, 80],\n    [-40, 83], [-55, 75], [-58, 68], [-55, 60],\n  ], // Greenland\n  [\n    [113, -22], [122, -18], [130, -12], [137, -12], [142, -11], [145, -15],\n    [153, -27], [150, -35], [140, -38], [131, -32], [115, -34], [113, -22],\n  ], // Australia\n  [\n    [-180, -63], [-150, -66], [-120, -66], [-90, -70], [-60, -66],\n    [-30, -66], [0, -66], [30, -66], [60, -66], [90, -66], [120, -66],\n    [150, -66], [180, -63], [180, -85], [-180, -85], [-180, -63],\n  ], // Antarctica\n];\n\n// Illustrative internal borders — not authoritative, just enough to gesture at\n// the \"country boundaries\" the spec asks for without a bundled GeoJSON dataset.\nconst INTERNAL_BORDERS = [\n  { name: \"Europe–Asia divide (illustrative, Ural Mtns)\", points: [[60, 70], [60, 50], [55, 40]] },\n  { name: \"Canada–United States (illustrative)\", points: [[-140, 49], [-95, 49], [-83, 45], [-70, 45]] },\n];\n\n// --- Projections --------------------------------------------------------------\nconst MERC_CLIP = 85; // clamp latitude — true Mercator diverges at the poles\n\nfunction toRad(deg) {\n  return (deg * Math.PI) / 180;\n}\n\nfunction mercatorXY(lon, lat) {\n  const phi = toRad(Math.max(-MERC_CLIP, Math.min(MERC_CLIP, lat)));\n  return { x: toRad(lon), y: Math.log(Math.tan(Math.PI / 4 + phi / 2)) };\n}\n\nfunction mollweideXY(lon, lat) {\n  const phi = toRad(lat);\n  const lam = toRad(lon);\n  let theta = phi;\n  if (Math.PI / 2 - Math.abs(phi) > 1e-9) {\n    for (let i = 0; i < 10; i++) {\n      const delta = (2 * theta + Math.sin(2 * theta) - Math.PI * Math.sin(phi)) / (2 + 2 * Math.cos(2 * theta));\n      theta -= delta;\n    }\n  } else {\n    theta = Math.sign(phi) * (Math.PI / 2);\n  }\n  return { x: ((2 * Math.SQRT2) / Math.PI) * lam * Math.cos(theta), y: Math.SQRT2 * Math.sin(theta) };\n}\n\nconst MERC_Y_LIMIT = mercatorXY(0, MERC_CLIP).y;\nconst PROJECTIONS = {\n  mercator: {\n    fn: mercatorXY,\n    range: { xMin: -Math.PI, xMax: Math.PI, yMin: -MERC_Y_LIMIT, yMax: MERC_Y_LIMIT },\n    latMin: -MERC_CLIP,\n    latMax: MERC_CLIP,\n  },\n  mollweide: {\n    fn: mollweideXY,\n    range: { xMin: -2 * Math.SQRT2, xMax: 2 * Math.SQRT2, yMin: -Math.SQRT2, yMax: Math.SQRT2 },\n    latMin: -90,\n    latMax: 90,\n  },\n};\n\n// Project a lon/lat pair into the data-space [x, y] pair for a given\n// projection kind. The cartesian2d grid/axis pair below does the linear\n// mapping from this data space into panel pixels — no manual rect math here.\nfunction projArr(lon, lat, kind) {\n  const p = PROJECTIONS[kind].fn(lon, lat);\n  return [p.x, p.y];\n}\n\nfunction meridian(lon, kind) {\n  const proj = PROJECTIONS[kind];\n  const pts = [];\n  for (let lat = proj.latMin; lat <= proj.latMax + 1e-6; lat += 2) pts.push([lon, lat]);\n  return pts;\n}\n\nfunction parallel(lat) {\n  return [[-180, lat], [180, lat]];\n}\n\n// --- Panel layout (CSS px, within the ANYPLOT_SIZE mount) -------------------\nconst MARGIN_X = 45;\nconst GAP_X = 55;\n// Mollweide panel is 2x as wide as the square Mercator panel — 3 width units total.\nconst PANEL_H = Math.floor((SIZE.width - 2 * MARGIN_X - GAP_X) / 3);\nconst PANEL_Y = 130;\nconst MERC_RECT = { x: MARGIN_X, y: PANEL_Y, w: PANEL_H, h: PANEL_H };\nconst MOLL_RECT = { x: MARGIN_X + PANEL_H + GAP_X, y: PANEL_Y, w: 2 * PANEL_H, h: PANEL_H };\nconst CAPTION_Y = PANEL_Y + PANEL_H + 40;\n\nconst MERIDIAN_LONS = [-150, -120, -90, -60, -30, 0, 30, 60, 90, 120, 150];\nconst PARALLEL_LATS = [-60, -30, 0, 30, 60];\nconst OCEAN_STYLE = { fill: t.elevatedBg, stroke: t.inkSoft, lineWidth: 2 };\n\n// --- Series data builders (idiomatic ECharts: cartesian2d grid + `lines` /\n// `custom` series, instead of hand-drawn `graphic.elements`) ------------------\nfunction graticuleLines(kind) {\n  const lines = [];\n  for (const lon of MERIDIAN_LONS) {\n    lines.push({ coords: meridian(lon, kind).map(([lo, la]) => projArr(lo, la, kind)) });\n  }\n  for (const lat of PARALLEL_LATS) {\n    lines.push({ coords: parallel(lat).map(([lo, la]) => projArr(lo, la, kind)) });\n  }\n  return lines;\n}\n\nfunction borderLines(kind) {\n  return INTERNAL_BORDERS.map((b) => ({\n    name: b.name,\n    coords: b.points.map(([lon, lat]) => projArr(lon, lat, kind)),\n  }));\n}\n\nconst LAND_DATA = LANDMASS_NAMES.map((name) => ({ name, value: name }));\n\nfunction landRenderItem(kind) {\n  return function (params, api) {\n    const points = LANDMASSES[params.dataIndex].map(([lon, lat]) => api.coord(projArr(lon, lat, kind)));\n    return { type: \"polygon\", shape: { points }, style: api.style() };\n  };\n}\n\nfunction panelGraphics(rect, label) {\n  return [\n    {\n      type: rect === MERC_RECT ? \"rect\" : \"ellipse\",\n      shape:\n        rect === MERC_RECT\n          ? { x: rect.x, y: rect.y, width: rect.w, height: rect.h }\n          : { cx: rect.x + rect.w / 2, cy: rect.y + rect.h / 2, rx: rect.w / 2, ry: rect.h / 2 },\n      style: OCEAN_STYLE,\n      silent: true,\n    },\n    {\n      type: \"text\",\n      left: rect.x + rect.w / 2,\n      top: rect.y - 34,\n      style: { text: label, fill: t.ink, font: \"600 16px sans-serif\", textAlign: \"center\" },\n    },\n  ];\n}\n\nconst graphicElements = [\n  ...panelGraphics(MERC_RECT, \"Mercator — conformal (shape-true, area-distorted)\"),\n  ...panelGraphics(MOLL_RECT, \"Mollweide — equal-area (area-true, shape-distorted)\"),\n  {\n    type: \"text\",\n    left: SIZE.width / 2,\n    top: CAPTION_Y,\n    style: {\n      text: \"Continent-level silhouettes with illustrative internal borders — not authoritative country boundaries\",\n      fill: t.inkSoft,\n      font: \"13px sans-serif\",\n      textAlign: \"center\",\n      opacity: 0.85,\n    },\n  },\n];\n\n// --- Chart --------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"map-projections · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 20,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n  },\n  tooltip: {\n    trigger: \"item\",\n    backgroundColor: t.elevatedBg,\n    borderColor: t.grid,\n    textStyle: { color: t.ink },\n    formatter: (p) => p.name,\n  },\n  graphic: { elements: graphicElements },\n  grid: [\n    { left: MERC_RECT.x, top: MERC_RECT.y, width: MERC_RECT.w, height: MERC_RECT.h, show: false },\n    { left: MOLL_RECT.x, top: MOLL_RECT.y, width: MOLL_RECT.w, height: MOLL_RECT.h, show: false },\n  ],\n  xAxis: [\n    { gridIndex: 0, type: \"value\", min: PROJECTIONS.mercator.range.xMin, max: PROJECTIONS.mercator.range.xMax, show: false },\n    { gridIndex: 1, type: \"value\", min: PROJECTIONS.mollweide.range.xMin, max: PROJECTIONS.mollweide.range.xMax, show: false },\n  ],\n  yAxis: [\n    { gridIndex: 0, type: \"value\", min: PROJECTIONS.mercator.range.yMin, max: PROJECTIONS.mercator.range.yMax, show: false },\n    { gridIndex: 1, type: \"value\", min: PROJECTIONS.mollweide.range.yMin, max: PROJECTIONS.mollweide.range.yMax, show: false },\n  ],\n  series: [\n    {\n      type: \"lines\",\n      coordinateSystem: \"cartesian2d\",\n      xAxisIndex: 0,\n      yAxisIndex: 0,\n      polyline: true,\n      silent: true,\n      z: 1,\n      lineStyle: { color: t.grid, width: 1, opacity: 0.9 },\n      data: graticuleLines(\"mercator\"),\n    },\n    {\n      type: \"lines\",\n      coordinateSystem: \"cartesian2d\",\n      xAxisIndex: 1,\n      yAxisIndex: 1,\n      polyline: true,\n      silent: true,\n      z: 1,\n      lineStyle: { color: t.grid, width: 1, opacity: 0.9 },\n      data: graticuleLines(\"mollweide\"),\n    },\n    {\n      type: \"custom\",\n      coordinateSystem: \"cartesian2d\",\n      xAxisIndex: 0,\n      yAxisIndex: 0,\n      z: 2,\n      itemStyle: { color: t.palette[0], borderColor: t.inkSoft, borderWidth: 1, opacity: 0.92 },\n      renderItem: landRenderItem(\"mercator\"),\n      data: LAND_DATA,\n    },\n    {\n      type: \"custom\",\n      coordinateSystem: \"cartesian2d\",\n      xAxisIndex: 1,\n      yAxisIndex: 1,\n      z: 2,\n      itemStyle: { color: t.palette[0], borderColor: t.inkSoft, borderWidth: 1, opacity: 0.92 },\n      renderItem: landRenderItem(\"mollweide\"),\n      data: LAND_DATA,\n    },\n    {\n      type: \"lines\",\n      coordinateSystem: \"cartesian2d\",\n      xAxisIndex: 0,\n      yAxisIndex: 0,\n      polyline: true,\n      silent: true,\n      z: 3,\n      lineStyle: { color: t.inkSoft, width: 1, type: \"dashed\", opacity: 0.55 },\n      data: borderLines(\"mercator\"),\n    },\n    {\n      type: \"lines\",\n      coordinateSystem: \"cartesian2d\",\n      xAxisIndex: 1,\n      yAxisIndex: 1,\n      polyline: true,\n      silent: true,\n      z: 3,\n      lineStyle: { color: t.inkSoft, width: 1, type: \"dashed\", opacity: 0.55 },\n      data: borderLines(\"mollweide\"),\n    },\n  ],\n});\n"}