{"id":52089052,"url":"https://github.com/rameerez/mappo","last_synced_at":"2026-08-04T04:30:39.435Z","repository":{"id":374241070,"uuid":"1319653027","full_name":"rameerez/mappo","owner":"rameerez","description":"Dotted world map web component — any resolution, city markers by name, six ambient animations, tilt, events. One 20 KB zero-dependency ES module, no build step.","archived":false,"fork":false,"pushed_at":"2026-08-01T23:04:58.000Z","size":131,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-08-02T00:10:52.162Z","etag":null,"topics":["custom-element","data-visualization","dot-map","dotted-map","esm","svg","web-component","world-map","worldmap","zero-dependencies"],"latest_commit_sha":null,"homepage":"https://rameerez.github.io/mappo/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rameerez.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null,"disclosure":null}},"created_at":"2026-08-01T22:08:29.000Z","updated_at":"2026-08-01T23:05:02.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/rameerez/mappo","commit_stats":null,"previous_names":["rameerez/mapamundi"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/rameerez/mappo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rameerez%2Fmappo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rameerez%2Fmappo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rameerez%2Fmappo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rameerez%2Fmappo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rameerez","download_url":"https://codeload.github.com/rameerez/mappo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rameerez%2Fmappo/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":36260999,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-08-04T02:00:06.901Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["custom-element","data-visualization","dot-map","dotted-map","esm","svg","web-component","world-map","worldmap","zero-dependencies"],"created_at":"2026-08-04T04:30:38.789Z","updated_at":"2026-08-04T04:30:39.426Z","avatar_url":"https://github.com/rameerez.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"assets/mappo-logo.webp\" alt=\"mappo\" width=\"440\"\u003e\n\u003c/p\u003e\n\n# mappo\n\n**A dotted world map as a zero-dependency web component.** Land dots derived\nfrom a packed bitmask at any resolution, a built-in city registry (just type\n`\"London\"`), shapes, tilt, pulse markers, hover/click events. One ESM file,\nno build step, no dependencies.\n\n```html\n\u003cscript type=\"module\" src=\"https://unpkg.com/mappo\"\u003e\u003c/script\u003e\n\n\u003cworld-map cities=\"London, Lagos, Singapore\" tilt=\"40\"\u003e\u003c/world-map\u003e\n```\n\nThat's the whole integration.\n\n## Why this exists\n\nEvery SaaS hero section eventually wants the dotted world with glowing city\nmarkers. The usual path is a designer's frozen SVG: thousands of hardcoded\nrectangles, cities placed by eye, one resolution forever. `mappo`\nderives the dots from a ~22 KB packed land bitmask instead — so resolution,\ndot shape, projection framing, and city markers are all runtime parameters,\nand \"add Nairobi\" is typing `Nairobi`.\n\n## Install\n\n```bash\nnpm install mappo\n```\n\nOr skip npm entirely — it's one file:\n\n```html\n\u003cscript type=\"module\" src=\"https://unpkg.com/mappo\"\u003e\u003c/script\u003e\n```\n\nRails with importmaps:\n\n```ruby\n# config/importmap.rb\npin \"mappo\", to: \"mappo.js\" # vendor dist/mappo.js\n```\n\n## The element\n\n```html\n\u003cworld-map\n  cities=\"London, Lagos, Singapore, New York\"\n  cols=\"140\"\n  dot-shape=\"circle\"\n  dot-color=\"#d3dce6\"\n  marker-color=\"#2262fe\"\n  marker-pulse=\"true\"   \u003c!-- animations are opt-in; default is a calm, static map --\u003e\n  tilt=\"40\"\n  animation=\"wave\"\n\u003e\u003c/world-map\u003e\n```\n\nAttributes are live — change one, the map re-renders. Interaction bubbles as\nDOM events:\n\n```js\nmap.addEventListener(\"worldmap:cityclick\", (e) =\u003e {\n  console.log(e.detail.name, e.detail.lat, e.detail.lon);\n});\n// also: worldmap:cityenter, :dotclick, :dotenter\n```\n\n## Globe mode\n\nThe same world, wrapped on a sphere and spinning:\n\n```html\n\u003cworld-map mode=\"globe\" cols=\"170\" tilt=\"18\" rotate-speed=\"4\"\n           dot-shape=\"square\" cities=\"Madrid, Nairobi, Tokyo\"\u003e\u003c/world-map\u003e\n```\n\nGlobe mode renders on canvas (a rotating globe re-projects every dot every\nframe — that's not SVG work), so the flat renderer's guarantees change\nshape: dots shrink and fade toward the limb, the back hemisphere is culled,\na hairline halo rings the sphere, and `tilt` becomes the *axial* tilt.\n`rotate-speed` is degrees per second; `0` parks it. The loop pauses when\nthe globe scrolls offscreen, and `prefers-reduced-motion` gets a single\nstatic frame instead of a spin.\n\nThe six animation modes work on the globe too — dots lift radially off\nthe surface (sparkle scales instead), driven by the same phase fields as\nthe flat renderer. Hover and click events fire with the same payloads as\nflat mode (canvas hit-testing through the inverse projection), and the\nglobe is grabbable: drag to spin it, flick for momentum, and the spin\nrelaxes back to `rotate-speed` on its own. Flat-only for now: marker\npulse. Custom SVG path dot shapes fall back to squares on canvas.\n\n## Pointing at places (v0.4)\n\nThree attributes turn the globe from a decoration into a *\"here\"*:\n\n```html\n\u003cworld-map mode=\"globe\" rotate-speed=\"0\"\n           focus=\"48.86,2.35\"\n           markers=\"Paris@48.86,2.35\"\n           marker-shape=\"pin\" marker-scale=\"4\" marker-pulse=\"true\"\n           highlight-color=\"#8fabe0\"\n           highlight-polygon='[[[51.1,2.5],[50.1,1.4],[49.4,-1.9],[48.6,-4.6],[47.3,-2.5],[46.2,-1.2],[43.4,-1.8],[42.5,3.0],[43.5,7.0],[46.4,6.8],[49.0,8.1],[51.1,2.5]]]'\n\u003e\u003c/world-map\u003e\n```\n\n- **`markers=\"Name@lat,lon;...\"`** — coordinate pins, no gazetteer lookup.\n  Semicolon-separated, `Name@` optional. They feed the same pipeline as\n  `cities` (which has always accepted `{ name, lat, lon }` objects from\n  JS — this attribute just gives markup the same power) and fire the same\n  events.\n- **`focus=\"lat,lon\"`** — the globe *starts facing* that point: the\n  initial spin angle brings the focus longitude to the front. With\n  `rotate-speed=\"0\"` it holds there; with a spin it's the opening frame.\n  Pair with `tilt` to bias the latitude toward the viewer.\n- **`marker-shape=\"pin\"`** — the map-pin silhouette (round head, punched\n  hole, anchored at the TIP — the point is the place, the head floats\n  above it). Draws on both renderers; `marker-pulse` pings at the anchor.\n- **`highlight-polygon`** + **`highlight-color`** — every land dot inside\n  the polygon draws in the highlight colour: the whole country or state\n  glows, not just the pin. The value is JSON rings of `[lat, lon]` pairs\n  (one ring or an array of rings — islands welcome). **mappo ships no\n  boundary data** on purpose: you supply the shape (Natural Earth's\n  public-domain admin polygons compact beautifully — a country is\n  typically 1–3\u0026nbsp;KB at the resolution a dot grid can even resolve).\n  Rings crossing the antimeridian are normalized automatically. Globe\n  mode only for now.\n\nThe highlight test runs once per geometry build, not per frame: flags\nparallel the point buffer index-for-index (the same discipline as the\nanimation phase fields — geometry arrays never reorder, parallel arrays\nannotate them), and the draw loop batches colour switches on flag runs.\n\n## Backdrop\n\nThree knobs fill the empty space, in either mode:\n\n```html\n\u003cworld-map mode=\"globe\" ocean-color=\"#e8eef5\" background=\"#f8fafc\"\n           globe-ring=\"true\"\u003e\u003c/world-map\u003e\n```\n\n- `dot-hover-color` defaults to **auto**: a contrast-aware shade of\n  `dot-color` — darker for light dots, lighter for dark ones — so hovers\n  never fall back to somebody else's gray. Set it (or `dot-hover-scale`)\n  to override.\n- `ocean-color` — water cells render as smaller filler dots in their own\n  shade (think off-white on light pages, off-dark on dark ones). In flat\n  mode this is a single SVG pattern — one node, any resolution, and it\n  patches as pure style. Default `none`.\n- `background` — a uniform fill behind everything: full-bleed rect in flat\n  mode, the planet disc in globe mode. Default `none`.\n- `globe-ring=\"true\"` — adds a hairline halo around the globe (off by default).\n\n## The JS API\n\n```js\nimport { WorldMap } from \"mappo\";\n\nconst map = new WorldMap(document.querySelector(\"#hero-map\"), {\n  cols: 140,                       // dots across the world — the resolution\n  latRange: [-58, 84],             // default framing cuts Antarctica\n  dotShape: \"circle\",              // \"circle\" | \"square\" | \"triangle\" | SVG path (24×24)\n  dotSize: 0.55,                   // fraction of a grid cell\n  dotColor: \"#d3dce6\",\n  dotHoverColor: \"#94a8bd\",\n  dotHoverScale: 2.2,\n  cities: [\n    \"Tokyo\", \"Berlin\",             // the built-in registry (~160 cities)\n    { name: \"HQ\", lat: 41.4, lon: 2.2, color: \"#ff9900\" } // or your own coords\n  ],\n  markerShape: \"circle\",\n  markerColor: \"#2262fe\",\n  markerPulse: false,\n  tilt: 40,                        // the lying-down hero look (rotateX, deg)\n  perspective: 1000,\n  animation: \"none\",                 // \"wave\" animates the whole matrix\n  cursor: \"default\",\n  markerCursor: \"pointer\",\n  onCityClick: ({ name }) =\u003e console.log(name)\n});\n\nmap.update({ markerColor: \"#ff3b30\" }); // re-render with new options\nmap.destroy();\n```\n\nLower-level pieces are exported too — `isLand(lat, lon)`, `project`,\n`cellCenter`, `snapToLand`, and the `CITIES` registry — if you want to build\nyour own renderer on the same data.\n\n## Styling\n\nThe component renders into light DOM with plain classes (`.wm-dot`,\n`.wm-marker`, `.wm-svg`, `.wm-tilt`) — your stylesheet wins. The built-in\nstyles are defaults, not law. `prefers-reduced-motion` disables all\nanimation automatically.\n\n## Design notes\n\n### Two renderers, on purpose\n\nThe flat map is SVG. The globe is canvas. This is not an accident of\nhistory or a migration in progress — each renderer matches the physics of\nits mode, and neither should become the other.\n\n**Why the flat map stays SVG:**\n\n1. **SVG-ness is a feature, not an implementation detail.** Dots are real\n   DOM elements: you restyle `.wm-dot` from your own stylesheet, markers\n   are focusable, hover states are plain CSS, everything shows up in\n   devtools, and the output is vector-crisp at any zoom and in print.\n   Every canvas map library forfeits all of that. It's the reason this one\n   is different.\n2. **The performance math favors SVG in flat's actual regime.** A static\n   SVG map costs *zero* per frame after render — and flat maps are static\n   almost all the time; they re-render only when options change, which the\n   differential update tiers make nearly free (style patches never touch\n   geometry). Animations run as CSS keyframes: compositor-eligible,\n   browser-scheduled, `prefers-reduced-motion` handled for free. A canvas\n   flat map would burn main-thread JavaScript every animated frame,\n   forever, to reproduce what the browser already does better.\n3. **SVG only loses above ~7k animated nodes** — which is exactly the\n   regime the density load gate and the cols cap already govern. The\n   escape hatch for extreme grids (cols ≫ 260) is a future opt-in\n   `renderer: \"canvas\"` behind the same options, built when someone\n   actually needs 500 cols — not a wholesale conversion.\n\n**Why the globe is canvas:**\n\nA rotating globe re-projects every dot every frame. That's thousands of\nper-frame position writes — as DOM attributes, it's the exact failure mode\nthe flat renderer's architecture exists to avoid; as canvas fills, it's\nnothing. The globe gives up SVG's styling hooks (and re-earns the\ninteractive ones through inverse-projection hit-testing, so events work\nthe same in both modes) in exchange for a renderer that can spin at max\nresolution without dropping frames.\n\nSame options, same events, same land data — `mode` just picks the\nrenderer whose physics fit.\n\n### The rest\n\n- **Equirectangular on purpose**: this is a *symbolic* map. Linear lat/lon\n  matches both the packed mask and everyone's mental world map.\n- **Coastal snapping**: city coordinates snap to the nearest land dot\n  (harbors sit in sea cells at coarse resolutions; a marker floating off\n  the coast looks broken).\n- **Globe mode is a renderer swap, not an API fork**: coordinates are\n  lat/lon everywhere, the option surface is shared, and `tilt` means \"lean\n  the world\" in both modes — CSS rotateX when flat, axial tilt when globe.\n\n## Performance\n\nMeasured, budgeted, and regression-tested (`demo/perf.html` runs scripted\nabuse with hard budgets; `test/` locks the update architecture). The rules\nof thumb the numbers produced:\n\n| you want | keep |\n|---|---|\n| an animated hero (`animation` on) | `cols ≤ 180` (≈4.5k dots) — full smoothness |\n| an animated map at higher density | the built-in load gate animates a baked subset above 4.5k/7k dots automatically |\n| maximum resolution (`cols` 200–260) | `animation=\"none\"` — static maps stay cheap at any size |\n\nResolution changes are debounced adaptively (spacing self-tunes to your\nmachine's measured frame cost), style/color/animation knobs never rebuild\ngeometry, and SVG stays the renderer up to 260 cols — dots are real,\nhoverable, restylable elements. A canvas mode for extreme grids is on the\nroadmap behind the same options.\n\n## Data\n\nLand shapes derived from [Natural Earth](https://www.naturalearthdata.com)\n(110m land polygons, public domain), rasterized into a 512×256 bitmask at\nbuild time by `scripts/generate-mask.js`. Regenerate any time; consumers\nnever run it.\n\n## Development\n\n```bash\nnode scripts/generate-mask.js   # refresh src/mask.js from Natural Earth\nnode scripts/build.js           # bundle src/ → dist/mappo.js\nnode --test test/               # the suite runs against dist/\n```\n\n## License\n\nMIT © rameerez. Land data: Natural Earth (public domain).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frameerez%2Fmappo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frameerez%2Fmappo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frameerez%2Fmappo/lists"}