{"spec_id":"map-tile-background","library":"echarts","language":"javascript","code":"// anyplot.ai\n// map-tile-background: Map with Tile Background\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-08-26\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Major U.S. landmark cities with annual visitor counts (millions).\nconst cities = [\n  { name: 'New York', lat: 40.7128, lon: -74.006, visitors: 8.2 },\n  { name: 'Los Angeles', lat: 34.0522, lon: -118.2437, visitors: 5.9 },\n  { name: 'Chicago', lat: 41.8781, lon: -87.6298, visitors: 4.1 },\n  { name: 'Houston', lat: 29.7604, lon: -95.3698, visitors: 2.3 },\n  { name: 'Phoenix', lat: 33.4484, lon: -112.074, visitors: 1.8 },\n  { name: 'San Francisco', lat: 37.7749, lon: -122.4194, visitors: 6.5 },\n  { name: 'Seattle', lat: 47.6062, lon: -122.3321, visitors: 3.2 },\n  { name: 'Denver', lat: 39.7392, lon: -104.9903, visitors: 2.7 },\n  { name: 'Miami', lat: 25.7617, lon: -80.1918, visitors: 5.4 },\n  { name: 'Boston', lat: 42.3601, lon: -71.0589, visitors: 3.9 },\n  { name: 'Atlanta', lat: 33.749, lon: -84.388, visitors: 2.9 },\n  { name: 'New Orleans', lat: 29.9511, lon: -90.0715, visitors: 3.4 },\n];\n\n// Equirectangular projection with a latitude-corrected longitude scale — the\n// simplest stand-in for the Web Mercator projection real slippy-map tiles use.\nconst meanLatRad = (cities.reduce((sum, c) => sum + c.lat, 0) / cities.length) * (Math.PI / 180);\nconst lonScale = Math.cos(meanLatRad);\nconst project = (lon, lat) => [lon * lonScale, lat];\n\nconst maxVisitors = Math.max(...cities.map(c => c.visitors));\nconst points = cities.map(c => {\n  const isTop = c.visitors === maxVisitors;\n  return {\n    name: c.name,\n    value: [...project(c.lon, c.lat), c.visitors],\n    isTop,\n    itemStyle: {\n      color: t.palette[0],\n      borderColor: isTop ? t.amber : t.pageBg,\n      borderWidth: isTop ? 3 : 2,\n    },\n    label: isTop ? { fontWeight: 'bold', color: t.amber } : {},\n  };\n});\n\n// Padded bounding box around the data, in projected units.\nconst xs = points.map(p => p.value[0]);\nconst ys = points.map(p => p.value[1]);\nconst padX = (Math.max(...xs) - Math.min(...xs)) * 0.12;\nconst padY = (Math.max(...ys) - Math.min(...ys)) * 0.12;\nconst xMin = Math.min(...xs) - padX;\nconst xMax = Math.max(...xs) + padX;\nconst yMin = Math.min(...ys) - padY;\nconst yMax = Math.max(...ys) + padY;\n\n// Simplified continental-U.S. coastline / border outline (hardcoded, low-fidelity\n// but geographically real — traces the Pacific coast, the Mexican border, the\n// Gulf and Atlantic coasts, and the Canadian border). Used both to draw a visible\n// outline over the tile backdrop and to classify each tile cell as land or water,\n// so the \"tile background\" carries genuine geographic context even offline.\nconst US_OUTLINE_LONLAT = [\n  [-124.7, 48.4], [-124.1, 44.6], [-124.0, 40.8], [-122.5, 37.8], [-120.6, 34.5],\n  [-117.2, 32.6], [-114.7, 32.5], [-111.0, 31.3], [-108.2, 31.3], [-106.5, 31.8],\n  [-104.9, 29.5], [-99.5, 26.4], [-97.4, 25.9], [-97.2, 27.8], [-95.3, 28.9],\n  [-93.8, 29.7], [-89.4, 29.2], [-85.0, 29.7], [-82.7, 27.8], [-81.8, 25.8],\n  [-80.2, 25.8], [-80.0, 26.7], [-81.5, 30.3], [-79.9, 32.8], [-77.9, 34.2],\n  [-76.5, 34.7], [-75.7, 35.2], [-76.0, 36.9], [-75.5, 38.3], [-74.0, 40.6],\n  [-71.0, 41.5], [-70.0, 42.0], [-70.2, 43.7], [-68.5, 44.3], [-67.0, 44.9],\n  [-68.3, 46.4], [-69.8, 47.3], [-71.0, 45.3], [-73.3, 45.0], [-76.0, 44.2],\n  [-79.2, 43.3], [-83.1, 42.3], [-84.5, 46.5], [-88.0, 48.0],\n  [-95.2, 49.0], [-104.0, 49.0], [-110.0, 49.0], [-116.0, 49.0], [-122.8, 49.0],\n  [-124.7, 48.4],\n];\nconst usOutline = US_OUTLINE_LONLAT.map(([lon, lat]) => project(lon, lat));\n\nfunction pointInPolygon(x, y, poly) {\n  let inside = false;\n  for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {\n    const [xi, yi] = poly[i];\n    const [xj, yj] = poly[j];\n    const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;\n    if (intersect) inside = !inside;\n  }\n  return inside;\n}\n\n// Tile grid: the render harness is offline (no fetch/CDN), so a real tile\n// provider (OpenStreetMap, CartoDB, satellite, …) cannot be loaded. Each cell\n// is classified land/water against the coastline outline above and shaded\n// accordingly, so the backdrop reads as a schematic basemap rather than a\n// content-free checkerboard.\nconst COLS = 18;\nconst ROWS = 10;\nconst cellW = (xMax - xMin) / COLS;\nconst cellH = (yMax - yMin) / ROWS;\nconst tiles = [];\nfor (let row = 0; row < ROWS; row++) {\n  for (let col = 0; col < COLS; col++) {\n    const x0 = xMin + col * cellW;\n    const y0 = yMin + row * cellH;\n    const isLand = pointInPolygon(x0 + cellW / 2, y0 + cellH / 2, usOutline);\n    tiles.push([x0, y0, x0 + cellW, y0 + cellH, isLand ? 1 : 0]);\n  }\n}\n\n// --- Init ---------------------------------------------------------------\nconst chart = echarts.init(document.getElementById('container'));\n\n// --- Option ---------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: 'transparent',\n  title: {\n    text: 'map-tile-background · javascript · echarts · anyplot.ai',\n    subtext: 'Marker size ∝ annual visitors (millions), ★ = top destination · schematic land/water tile backdrop (offline render, no live tile provider)',\n    left: 'center',\n    top: 24,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 'bold' },\n    subtextStyle: { color: t.inkSoft, fontSize: 14 },\n  },\n  grid: { left: 50, right: 50, top: 150, bottom: 60 },\n  xAxis: { type: 'value', min: xMin, max: xMax, show: false },\n  yAxis: { type: 'value', min: yMin, max: yMax, show: false },\n  // Interactive zoom/pan (scroll to zoom, drag to pan) — invisible in the static\n  // PNG but wired up for the exported HTML detail view, per the spec's request\n  // for interactive libraries to support exploring the map at different scales.\n  dataZoom: [{ type: 'inside', xAxisIndex: 0, yAxisIndex: 0, filterMode: 'none' }],\n  graphic: {\n    elements: [\n      {\n        type: 'text',\n        right: 40,\n        bottom: 20,\n        style: {\n          text: 'Basemap attribution: offline schematic render — live tile providers (OpenStreetMap, CartoDB, satellite) unavailable',\n          fill: t.inkSoft,\n          fontSize: 11,\n        },\n      },\n    ],\n  },\n  series: [\n    {\n      // Land/water tile backdrop — see comment above.\n      type: 'custom',\n      coordinateSystem: 'cartesian2d',\n      renderItem(params, api) {\n        const p0 = api.coord([api.value(0), api.value(1)]);\n        const p1 = api.coord([api.value(2), api.value(3)]);\n        const isLand = api.value(4) === 1;\n        return {\n          type: 'rect',\n          shape: {\n            x: Math.min(p0[0], p1[0]),\n            y: Math.min(p0[1], p1[1]),\n            width: Math.abs(p1[0] - p0[0]),\n            height: Math.abs(p1[1] - p0[1]),\n          },\n          style: {\n            // Semantic land/water tint (ochre for land, blue for water) instead of a\n            // near-invisible elevatedBg-vs-transparent pairing, so the schematic\n            // basemap reads as terrain rather than an empty grid.\n            fill: isLand ? t.palette[3] : t.palette[2],\n            stroke: t.grid,\n            lineWidth: 1,\n            opacity: isLand ? 0.16 : 0.22,\n          },\n        };\n      },\n      data: tiles,\n      encode: { x: 0, y: 1 },\n      silent: true,\n      z: 1,\n    },\n    {\n      // Coastline/border outline traced on top of the tile grid.\n      type: 'custom',\n      coordinateSystem: 'cartesian2d',\n      renderItem(params, api) {\n        const pathPoints = usOutline.map(([x, y]) => api.coord([x, y]));\n        return {\n          type: 'polyline',\n          shape: { points: pathPoints },\n          style: { stroke: t.inkSoft, lineWidth: 1.5, fill: 'none', opacity: 0.55 },\n        };\n      },\n      data: [0],\n      silent: true,\n      z: 2,\n    },\n    {\n      type: 'scatter',\n      coordinateSystem: 'cartesian2d',\n      data: points,\n      encode: { x: 0, y: 1 },\n      symbolSize: val => 24 + val[2] * 6,\n      itemStyle: { color: t.palette[0], borderColor: t.pageBg, borderWidth: 2 },\n      label: {\n        show: true,\n        formatter: p => (p.data.isTop ? '★ ' : '') + p.name,\n        position: 'top',\n        distance: 10,\n        color: t.ink,\n        fontSize: 15,\n      },\n      labelLayout: { hideOverlap: true, moveOverlap: 'shiftY' },\n      z: 3,\n    },\n  ],\n});\n"}