← Diagrams & maps

Dotted World Map

A world map drawn as a grid of dots, gently twinkling. One by one your locations light up: the dots around each one glow, a ring spreads out across the map and its name pops up above it. Lit places stay lit, so by the end your whole network glows.

San Francisco New York São Paulo London HQ Nairobi Dubai Singapore Tokyo Sydney

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-dotted-world-map to an element. It fills the width and keeps the map’s shape.
  3. List your places in data-locations, separated by commas, or add a child per place with data-place and its label as text.

Options

Add any of these attributes to the same element.

data-locationsPlaces to light, in order, separated by commas.
data-placeOn a child: a place to light. Its text is the label shown.
data-colorColour of the glow. Default #a855f7.
data-dotColour of the map’s dots. Default #cbbfe6.
data-stepTime between places. Default 1300 ms.
data-gapSpacing between dots, in px. Default: about 150 dots across.
data-loopDim the map and light the places again.

Places are city names (London, New York, Tokyo, Sydney and about 40 more big cities) or a latitude and longitude such as 48.86 2.35. It is drawn on a canvas and pauses off screen.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Dotted World Map: a world map drawn as a grid of dots on a canvas.
    // One by one your locations light up: the dots around each one glow,
    // a ring spreads out and its name pops up. Lit places stay lit, then the
    // map dims and starts over.
    // Usage:
    //   <div data-dotted-world-map data-locations="London, New York, Tokyo"></div>
    // or with your own labels:
    //   <div data-dotted-world-map>
    //     <span data-place="London">Head office</span>
    //     <span data-place="1.29 103.85">Singapore team</span>
    //   </div>
    // Places are city names from the list below or "lat lon".
    // Optional: data-color (glow colour, default #a855f7), data-dot (dot
    // colour, default #cbbfe6), data-step (ms between locations, default
    // 1300), data-loop (dim and light them again), data-gap (dot spacing
    // in px, default fits about 150 dots across).
    var DEFAULTS = { color: '#a855f7', dot: '#cbbfe6', step: 1300 };

    // Simplified coastlines: polygons separated by |, as "lon lat" pairs.
    var LAND = '-168 66 -162 70 -156 71.3 -141 69.6 -128 70 -115 68.8 -95 72 -82 73.5 -80 63 -94 59 -92 57 -82 55 -79 51.5 -76 56 -78 62.5 -70 61 -64 60 -61 56 -56 52 -60 47 -66 45 -70 43.5 -70 41.5 -74 40.5 -76 38 -76 35 -81 31.5 -80 27 -80.5 25.2 -82.5 27.5 -84 30 -89 30.2 -94 29.6 -97.5 27 -97.5 22 -95 19 -91 18.6 -90.5 21 -87 21.5 -88 16 -84 15.5 -83.5 11 -79.5 9 -77.5 8.5 -80 7.5 -83 8.5 -86 11.5 -92 14.5 -96 15.8 -101 17.5 -105.5 20.5 -105.5 23.5 -109 26 -112.8 31.3 -114.7 30 -112 25 -109.5 23 -110.5 24.5 -114 28 -115 30.5 -117 32.5 -120.5 34.5 -122.5 37.5 -124 40.5 -124 46 -123 49 -127.5 50.5 -131 54 -134 58 -140 59.8 -147 60.5 -152 59 -158 56.5 -164 54.6 -158 58.5 -162 60 -165 62.5 -164.5 64.5|-73 78 -60 82 -30 83.5 -18 81 -20 75 -22 70 -32 68 -40 65 -43 60 -48 61 -52 65 -54 69 -58 75.5 -68 76.5|-80 73.5 -68 70 -62 66.5 -65 63 -72 63.5 -78 67 -90 71 -85 73.8|-95 74 -80 76 -75 79 -62 82 -90 82.5 -105 79 -118 76 -110 73 -100 72.5|-118 69 -102 68.5 -101 73 -115 73.5 -125 71.8|-77.5 8.5 -72 12 -64 10.8 -60 8.5 -57 6 -52 5 -50 1.8 -48 -1 -44 -2.5 -38 -4 -35 -7 -35 -9 -39 -13.5 -39 -17.5 -40 -20.5 -42 -23 -46 -24 -48.5 -26 -48.5 -28.5 -53 -33.5 -57 -36 -57 -38.5 -62 -39 -65 -41 -65 -45 -67.5 -46.5 -66 -48 -69 -51 -68.5 -53 -71 -54 -74 -52 -75.5 -48 -73.5 -43 -73.5 -37 -71.5 -31 -70.3 -18.5 -75.5 -15 -78 -10 -81.2 -5.5 -80 -2 -80 1 -77 4 -77.5 6.5|-9.5 37 -9 43 -2 43.5 -1.5 46.5 -4.7 48.5 -1.5 48.7 1.5 50.5 4 51.5 5 53.3 8.5 53.8 8.5 57 10.5 57.7 10.5 54.5 14 54 19 54.5 21.5 57.5 24 57.3 23.5 59.3 29 60 22.5 60.5 21.5 63 25.5 65 22 65.8 17.5 62.5 19 60 16.5 56.5 13 55.5 11 58.5 8 58 5.5 58.8 5 62 10 64 14 67.5 19 70 25 71 31 70 40 67.5 41 66 34 64.6 37 64.2 44 66 44 68.5 53 68.5 59 68.5 67 69 69 72.8 73 72.5 72 66.5 78 72.5 83 70.5 87 73.8 100 76 105 77.7 113 73.7 127 73.5 131 71 140 72.5 150 71.5 160 70.6 170 70 180 69 180 65 178.5 64.5 175 62 170 60 163 59.8 163.5 56 156.7 51 155.5 57.5 162 61.7 155 59.3 143 59.4 136 54.5 141 52.5 140 48 135 43.5 131 42.6 129.5 40.5 129.5 36 126.5 34.5 126 37.7 124.5 39.8 121.5 39 122 40.5 118 39.2 119 37 122.5 37 120 34.5 121.8 31.5 122 29 118.5 24.5 113.5 22.2 109.5 21.5 108 21.5 106.5 20 105.8 18.5 108.5 15 109.3 11.5 105 8.6 104.7 10.5 100.5 13.5 99.2 10 100.5 7.2 103.5 1.3 101 2.8 98.5 8 98.3 12.5 97.5 16.5 94.5 16 94 19 92 22 90 22 86.5 20 85 19.5 80.3 15.5 80 10.3 77.5 8 76 10 73 17 72.5 21 69 22.5 67 24.5 62 25.2 57 25.7 56.3 27 54 26.8 51.5 27.9 50 30 48 30 48.5 28 51.5 24.5 55 22.5 58.8 22.5 59.8 22 57 18.8 52 16 45 12.8 43 13 42.6 15.5 39 21.5 35.5 27.5 32.6 30 34.5 31.8 35 32.5 36 35.8 32 36.2 29 36.7 26.5 39.5 29 41 28 42 28.5 43.5 30 45.5 33.5 44.5 36 45.3 39 47 38 45 41.5 41.5 36 41.5 33 42 29 41.2 26 40.8 23 40.2 24 38 22 36.5 21 38.5 19.5 41.5 19 42.5 13.5 45.5 12.3 44.5 16 41.5 18.5 40.2 16 38 15.7 40 12 42 10 44 8.5 44.4 6 43.1 3 43.3 3.3 41.9 0 39.5 -0.5 38 -2 36.7 -5.3 36 -6.5 36.9 -8.8 37|-6 35.8 -2 35.1 3 36.8 10 37.3 11 35.5 10 33.8 15 32.3 20 30.8 20 32.5 25 31.8 29 30.9 32.3 31.3 34 27.5 37 22 38.5 18 43 12.6 44 10.4 51.2 11.8 51 10.4 49 6 46 2 42 -1 40 -3 39 -6.5 40.5 -10.5 40.5 -15 37 -17.5 35.5 -21 35.5 -24 32.8 -26 32.5 -28.6 30 -31.3 27 -33.6 22 -34.1 18.5 -34.2 18 -31.5 15 -27 14.5 -22.5 11.8 -17 13.5 -12 12 -6 9 -1 9.5 3.8 8 4.4 5.5 4.2 3 6.3 -2 4.8 -7.5 4.4 -12 7 -15 10.8 -17.1 14.7 -16 19 -16.5 22 -13 27.5 -10 29.5 -9.5 32.5|44 -25 47 -25 50.5 -15.5 49.3 -12 44.5 -16 43.3 -22|-5.7 50 1.5 51.2 1.7 52.7 -0.5 54.5 -2 55.9 -1.8 57.6 -3 58.6 -5 58.6 -6.2 56.5 -5 55 -3 54 -4.6 53.3 -5 51.6|-10 51.6 -6 52 -6 54 -7.5 55.3 -10 54.2|-24 65.5 -22 66.4 -15 66.5 -13.5 65 -18 63.4 -22.5 63.8|130 31 131.5 31.5 132 33.8 135 33.5 136.8 34.5 140 35 141 38 142 40 141.4 41.4 140 40.3 139.8 38 137 37 133 35.5 130.9 34|140 41.5 141.8 42.5 145.5 43.3 142 45.5|79.8 6 81.8 7.5 80 9.8|95.3 5.6 98 4 104 -2 106 -5.8 102 -4 98.5 1|109 2 111 1 116.5 7 119 5 117.5 1 116.3 -3.5 114.5 -4 110.5 -3|105.5 -6.5 114.5 -7.7 114.4 -8.7 108 -7.8|119 -5.5 120.5 0.7 125 1.5 121 -1 123 -4.5|131 -1.3 134 -0.8 138 -1.7 145 -4.5 151 -10.5 147 -10.2 143 -9 141 -9.2 138 -8.3 137.5 -5 133 -4|120 18.5 122.3 18.4 124.3 12.5 126.5 7 125.5 5.6 122 7 123.5 10 121 13.5 120.5 16|113.5 -22 114 -26 115 -34 118 -35 123.5 -33.8 129 -31.6 131.5 -31.5 135 -34.8 138 -35.5 140 -37.8 144 -38.5 146.3 -39 150 -37.5 151.3 -34 153.5 -28.5 153 -25.5 149 -21 145.5 -15 143.5 -14 142.5 -10.7 141.5 -13 141.5 -16.5 140 -17.5 136 -15 136.8 -12.2 132.5 -11.5 130 -13.3 129.5 -15 127 -14 124 -16.3 122 -18 119 -20 116 -21|144.6 -40.7 148.3 -40.9 148 -43 146 -43.6|172.7 -34.5 178.5 -37.7 176.8 -40 174.8 -41.4 173 -39.3 174.5 -37|172.5 -40.6 174 -41.7 171 -44.3 169 -46.7 166.5 -46 168.3 -44|-85 21.8 -81 23.1 -77 21 -74.2 20.2 -77.5 19.9 -80.5 21.8|-74.4 18.5 -72.8 19.9 -68.4 19 -70 18.2';

    var CITIES = {
      'London': [-0.13, 51.5], 'Paris': [2.35, 48.86], 'Berlin': [13.4, 52.52], 'Madrid': [-3.7, 40.42],
      'Rome': [12.5, 41.9], 'Amsterdam': [4.9, 52.37], 'Stockholm': [18.07, 59.33], 'Moscow': [37.6, 55.75],
      'Istanbul': [28.98, 41.01], 'Reykjavik': [-21.9, 64.15], 'Cairo': [31.24, 30.04], 'Lagos': [3.38, 6.52],
      'Nairobi': [36.82, -1.29], 'Johannesburg': [28.05, -26.2], 'Cape Town': [18.42, -33.92], 'Dubai': [55.27, 25.2],
      'Mumbai': [72.88, 19.08], 'Delhi': [77.2, 28.6], 'Bangkok': [100.5, 13.75], 'Singapore': [103.82, 1.35],
      'Jakarta': [106.85, -6.2], 'Hong Kong': [114.17, 22.32], 'Shanghai': [121.47, 31.23], 'Beijing': [116.4, 39.9],
      'Seoul': [126.98, 37.57], 'Tokyo': [139.69, 35.69], 'Sydney': [151.21, -33.87], 'Melbourne': [144.96, -37.81],
      'Auckland': [174.76, -36.85], 'Honolulu': [-157.86, 21.3], 'Vancouver': [-123.12, 49.28], 'Seattle': [-122.33, 47.6],
      'San Francisco': [-122.42, 37.77], 'Los Angeles': [-118.24, 34.05], 'Mexico City': [-99.13, 19.43], 'Chicago': [-87.63, 41.88],
      'Toronto': [-79.38, 43.65], 'New York': [-74.0, 40.71], 'Miami': [-80.19, 25.76], 'Bogotá': [-74.07, 4.71],
      'Lima': [-77.04, -12.05], 'São Paulo': [-46.63, -23.55], 'Rio de Janeiro': [-43.17, -22.9], 'Buenos Aires': [-58.38, -34.6]
    };

    function plain(t) { return t.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''); }

    // A city name from the list above, or "lat lon" such as "48.86 2.35".
    // Returns [lon, lat, name] or null.
    function place(text) {
      text = text.trim();
      var m = /^(-?[\d.]+)[\s,]+(-?[\d.]+)$/.exec(text);
      if (m) return [+m[2], +m[1], ''];
      for (var k in CITIES) if (plain(k) === plain(text)) return [CITIES[k][0], CITIES[k][1], k];
      return null;
    }

    var css =
      ':where(.dwm){position:relative;width:100%;aspect-ratio:360/142}' +
      '.dwm{line-height:1.2}' +
      '.dwm-canvas{position:absolute;inset:0;width:100%;height:100%;display:block}' +
      '.dwm-label{position:absolute;translate:-50% -100%;margin-top:-.7em;white-space:nowrap;padding:.2em .6em;border-radius:99px;background:#fff;color:#2a1d40;font-weight:700;font-size:.82em;box-shadow:0 .15em .6em rgba(40,20,80,.18);pointer-events:none;opacity:0}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    var polys;
    function onLand(lon, lat) {
      if (!polys) {
        polys = LAND.split('|').map(function (p) {
          var n = p.split(' ').map(Number), pts = [];
          for (var i = 0; i < n.length; i += 2) pts.push([n[i], n[i + 1]]);
          return pts;
        });
      }
      var inside = false;
      for (var q = 0; q < polys.length; q++) {
        var p = polys[q];
        for (var i = 0, j = p.length - 1; i < p.length; j = i++) {
          if ((p[i][1] > lat) !== (p[j][1] > lat) && lon < (p[j][0] - p[i][0]) * (lat - p[i][1]) / (p[j][1] - p[i][1]) + p[i][0]) inside = !inside;
        }
      }
      return inside;
    }

    function rgb(hex) {
      var h = hex.replace('#', '');
      if (h.length === 3) h = h.replace(/./g, '$&$&');
      var n = parseInt(h, 16);
      return [n >> 16 & 255, n >> 8 & 255, n & 255];
    }

    function setup(el) {
      var d = el.dataset;
      var s = (el._dwm = { visible: false, raf: 0, spots: [], i: 0, rings: [] });
      el.classList.add('dwm');
      s.color = rgb(d.color || DEFAULTS.color);
      s.dot = rgb(d.dot || DEFAULTS.dot);
      s.step = +d.step || DEFAULTS.step;
      var list = [];
      Array.from(el.children).forEach(function (k) {
        var p = place(k.getAttribute('data-place') || '');
        if (p) list.push([p[0], p[1], k.textContent.trim() || p[2]]);
        k.remove();
      });
      (d.locations || '').split(',').forEach(function (t) {
        var p = place(t);
        if (p) list.push(p);
      });
      s.spots = list.map(function (p) { return { lon: p[0], lat: p[1], name: p[2], level: 0, flash: 0 }; });
      s.canvas = document.createElement('canvas');
      s.canvas.className = 'dwm-canvas';
      s.canvas.setAttribute('aria-hidden', 'true');
      el.appendChild(s.canvas);
      s.ctx = s.canvas.getContext('2d');
      s.label = document.createElement('span');
      s.label.className = 'dwm-label';
      s.label.setAttribute('aria-hidden', 'true');
      el.appendChild(s.label);
      el.setAttribute('role', 'img');
      if (!el.getAttribute('aria-label')) {
        el.setAttribute('aria-label', 'World map' + (list.length ? ' showing ' + list.map(function (p) { return p[2]; }).filter(Boolean).join(', ') : ''));
      }
      resize(el);
      new ResizeObserver(function () {
        if (Math.abs(el.clientWidth - s.w) > 2 || Math.abs(el.clientHeight - s.h) > 2) { resize(el); if (!s.raf) draw(el, 0); }
      }).observe(el);
    }

    // Fit the 360 x 142 degree map inside the box and lay the dots out.
    function resize(el) {
      var s = el._dwm, dpr = Math.min(2, window.devicePixelRatio || 1);
      s.w = el.clientWidth;
      s.h = el.clientHeight;
      s.canvas.width = Math.round(s.w * dpr);
      s.canvas.height = Math.round(s.h * dpr);
      s.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      var k = Math.min(s.w / 360, s.h / 142);
      s.k = k;
      s.ox = (s.w - 360 * k) / 2;
      s.oy = (s.h - 142 * k) / 2;
      s.gap = +el.dataset.gap || Math.max(3, 360 * k / 150);
      s.dots = [];
      var step = s.gap / k;
      for (var lat = 84 - step / 2; lat > -58; lat -= step) {
        for (var lon = -180 + step / 2; lon < 180; lon += step) {
          if (onLand(lon, lat)) s.dots.push([s.ox + (lon + 180) * k, s.oy + (84 - lat) * k, Math.random() * 6.28]);
        }
      }
      s.spots.forEach(function (p) { p.x = s.ox + (p.lon + 180) * k; p.y = s.oy + (84 - p.lat) * k; });
      placeLabel(s);
    }

    function placeLabel(s) {
      var p = s.active;
      if (!p) return;
      var half = s.label.offsetWidth / 2 + 2;
      s.label.style.left = Math.max(half, Math.min(s.w - half, p.x)) + 'px';
      s.label.style.top = p.y + 'px';
    }

    function draw(el, time) {
      var s = el._dwm, ctx = s.ctx, r0 = s.gap * 0.36, reach = s.gap * 3.2;
      ctx.clearRect(0, 0, s.w, s.h);
      var lit = s.spots.filter(function (p) { return p.level > 0.01; });
      for (var i = 0; i < s.dots.length; i++) {
        var dot = s.dots[i], glow = 0;
        for (var j = 0; j < lit.length; j++) {
          var p = lit[j], dx = dot[0] - p.x, dy = dot[1] - p.y, dd = (dx * dx + dy * dy) / (reach * reach * (1 + p.flash));
          if (dd < 4) glow += p.level * (1 + p.flash) * Math.exp(-dd);
        }
        glow = Math.min(1, glow);
        var tw = 0.82 + 0.18 * Math.sin(time * 0.0015 + dot[2]);
        var c0 = s.dot, c1 = s.color;
        ctx.fillStyle = 'rgb(' + Math.round(c0[0] + (c1[0] - c0[0]) * glow) + ',' + Math.round(c0[1] + (c1[1] - c0[1]) * glow) + ',' + Math.round(c0[2] + (c1[2] - c0[2]) * glow) + ')';
        ctx.globalAlpha = glow > 0.02 ? 0.55 + 0.45 * glow : tw;
        ctx.beginPath();
        ctx.arc(dot[0], dot[1], r0 * (1 + glow * 0.45), 0, Math.PI * 2);
        ctx.fill();
      }
      var col = 'rgb(' + s.color.join(',') + ')';
      s.rings.forEach(function (r) {
        ctx.globalAlpha = (1 - r.t) * 0.8;
        ctx.strokeStyle = col;
        ctx.lineWidth = Math.max(1.2, s.gap * 0.3);
        ctx.beginPath(); ctx.arc(r.p.x, r.p.y, s.gap + r.t * s.gap * 9, 0, Math.PI * 2); ctx.stroke();
      });
      lit.forEach(function (p) {
        ctx.globalAlpha = p.level;
        ctx.fillStyle = '#fff';
        ctx.beginPath(); ctx.arc(p.x, p.y, s.gap * 0.75, 0, Math.PI * 2); ctx.fill();
        ctx.fillStyle = col;
        ctx.beginPath(); ctx.arc(p.x, p.y, s.gap * 0.5, 0, Math.PI * 2); ctx.fill();
      });
      ctx.globalAlpha = 1;
    }

    function light(el) {
      var s = el._dwm, p = s.spots[s.i];
      if (!p) {
        if (el.dataset.loop === undefined) { s.done = true; return; }
        s.i = 0;
        anime.animate(s.label, { opacity: 0, duration: 300 });
        anime.animate(s.spots, { level: 0, flash: 0, duration: 900, ease: 'inOut(2)', delay: s.step });
        s.timer = setTimeout(function () { light(el); }, s.step + 1300);
        return;
      }
      s.i++;
      s.active = p;
      anime.animate(p, { level: [0, 1], duration: 500, ease: 'out(2)' });
      anime.animate(p, { flash: [1.4, 0], duration: 1400, ease: 'out(3)' });
      s.rings.push({ p: p, t: 0 });
      if (p.name && !el.closest('[aria-hidden="true"]')) {
        s.label.textContent = p.name;
        placeLabel(s);
        anime.animate(s.label, { opacity: [0, 1], y: [8, 0], duration: 380, ease: 'outBack(2)' });
      }
      s.timer = setTimeout(function () { light(el); }, s.step);
    }

    function frame(el, now) {
      var s = el._dwm;
      if (!s.visible) { s.raf = 0; return; }
      var dt = s.last ? Math.min(0.05, (now - s.last) / 1000) : 0;
      s.last = now;
      s.rings = s.rings.filter(function (r) { r.t += dt / 1.4; return r.t < 1; });
      draw(el, now);
      s.raf = requestAnimationFrame(function (t) { frame(el, t); });
    }

    function init() {
      var still = reduced();
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          var el = e.target, s = el._dwm;
          s.visible = e.isIntersecting;
          if (!s.visible) return;
          if (!s.started) { s.started = true; s.timer = setTimeout(function () { light(el); }, 400); }
          if (!s.raf) { s.last = 0; s.raf = requestAnimationFrame(function (t) { frame(el, t); }); }
        });
      }, { threshold: 0.1 });
      document.querySelectorAll('[data-dotted-world-map]').forEach(function (el) {
        if (el._dwm) return;
        setup(el);
        if (still) {
          el._dwm.spots.forEach(function (p) { p.level = 1; });
          draw(el, 0);
          return;
        }
        draw(el, 0);
        io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-dotted-world-map data-loop>
  <span data-place="London">London HQ</span>
  <span data-place="New York">New York</span>
  <span data-place="1.29 103.85">Singapore team</span>
</div>