← Diagrams & maps

Zoom to Location

The map flies down from the whole world to your city, the way map apps do: it speeds up and slows down smoothly, lines of latitude and longitude slide past and nearby cities fade in as it gets close. A pin drops on the spot with its name, and a readout in the corner shows the position and zoom.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-zoom-to-location to an empty element. It fills the width at 16:9, or give it a height.
  3. Name the place in data-places. List several, separated by commas, and it hops from one to the next.

Options

Add any of these attributes to the same element.

data-placesWhere to fly, in order. Default London.
data-zoomHow far in to go, as a multiple of the world view. Default 7.
data-durationTime for each flight. Default 2600 ms.
data-holdTime to stay at each place. Default 1800 ms.
data-colorColour of the pin. Default #7c3aed.
data-loopZoom back out to the world and fly again.

Places are city names (London, Paris, New York, Tokyo, Sydney, Dubai, São Paulo and about 40 more) or a latitude and longitude such as 48.86 2.35. The coastlines are simplified, so it suits a zoom to a city’s region rather than its streets.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Zoom to Location: a world map flies smoothly down from the whole world
    // to a city, the way map apps do. Lines of latitude and longitude slide
    // past, nearby cities fade in as it gets close, and a pin drops on the
    // spot with its name. Give it several places and it hops between them,
    // pulling back out a little on the way.
    // Usage:
    //   <div data-zoom-to-location data-places="London"></div>
    // Optional: data-places (comma-separated; city names from the list below
    // or "lat lon", e.g. "48.86 2.35"; default London), data-zoom (how far
    // in, as a multiple of the world view, default 7), data-duration (ms
    // for each flight, default 2600), data-hold (ms at each place, default
    // 1800), data-color (pin colour, default #7c3aed), data-loop (zoom back
    // out to the world and fly again).
    var DEFAULTS = { zoom: 7, duration: 2600, hold: 1800, color: '#7c3aed' };
    var NS = 'http://www.w3.org/2000/svg';

    // 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(.ztl){position:relative;width:100%;aspect-ratio:16/9}' +
      '.ztl{overflow:hidden;border-radius:.8em;line-height:1.2;background:#dfe9fb}' +
      '.ztl-svg{position:absolute;inset:0;width:100%;height:100%;display:block}' +
      '.ztl-pin{position:absolute;left:50%;top:50%;width:1.7em;height:2.3em;margin:-2.3em 0 0 -.85em;opacity:0;transform-origin:50% 100%;filter:drop-shadow(0 .15em .15em rgba(30,10,60,.3))}' +
      '.ztl-pin svg{display:block;width:100%;height:100%}' +
      '.ztl-ring{position:absolute;left:50%;top:50%;width:2em;height:2em;margin:-1em 0 0 -1em;border-radius:50%;border:.15em solid var(--ztl-c);box-sizing:border-box;opacity:0}' +
      '.ztl-name{position:absolute;left:50%;top:50%;margin-top:.5em;translate:-50% 0;padding:.25em .7em;border-radius:99px;background:#fff;color:#1f1530;font-weight:700;white-space:nowrap;box-shadow:0 .2em .7em rgba(30,10,60,.2);opacity:0}' +
      '.ztl-hud{position:absolute;left:.7em;bottom:.6em;padding:.2em .55em;border-radius:.4em;background:rgba(255,255,255,.75);color:#4a3d63;font-size:.75em;font-variant-numeric:tabular-nums;white-space:nowrap}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

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

    function mk(tag, attrs, parent) {
      var n = document.createElementNS(NS, tag);
      for (var k in attrs) n.setAttribute(k, attrs[k]);
      if (parent) parent.appendChild(n);
      return n;
    }

    // Equirectangular: x = lon + 180, y = 90 - lat. Coastlines are drawn as
    // smooth curves through the midpoints of each edge.
    function landPath() {
      return LAND.split('|').map(function (poly) {
        var n = poly.split(' ').map(Number), pts = [];
        for (var i = 0; i < n.length; i += 2) pts.push([n[i] + 180, 90 - n[i + 1]]);
        var mid = function (a, b) { return ((a[0] + b[0]) / 2).toFixed(2) + ' ' + ((a[1] + b[1]) / 2).toFixed(2); };
        var d = 'M' + mid(pts[pts.length - 1], pts[0]);
        for (var k = 0; k < pts.length; k++) d += 'Q' + pts[k][0] + ' ' + pts[k][1] + ' ' + mid(pts[k], pts[(k + 1) % pts.length]);
        return d + 'Z';
      }).join('');
    }

    function grid() {
      var d = '';
      for (var lon = -180; lon <= 180; lon += 10) d += 'M' + (lon + 180) + ' 0V180';
      for (var lat = -80; lat <= 80; lat += 10) d += 'M0 ' + (90 - lat) + 'H360';
      return d;
    }

    function setup(el) {
      var d = el.dataset;
      var s = (el._ztl = { cities: [] });
      el.classList.add('ztl');
      el.style.setProperty('--ztl-c', d.color || DEFAULTS.color);
      s.places = (d.places || 'London').split(',').map(place).filter(Boolean);
      if (!s.places.length) s.places = [place('London')];
      s.zoom = +d.zoom || DEFAULTS.zoom;
      var svg = (s.svg = mk('svg', { class: 'ztl-svg', 'aria-hidden': 'true', preserveAspectRatio: 'xMidYMid slice' }));
      mk('rect', { x: -400, y: -300, width: 1160, height: 780, fill: '#dfe9fb' }, svg);
      mk('path', { d: grid(), stroke: '#c3d3f0', 'stroke-width': 1, fill: 'none', 'vector-effect': 'non-scaling-stroke' }, svg);
      mk('path', { d: landPath(), fill: '#f4f0fb', stroke: '#c4b5e8', 'stroke-width': 1.2, 'vector-effect': 'non-scaling-stroke', 'stroke-linejoin': 'round' }, svg);
      var cg = mk('g', {}, svg);
      Object.keys(CITIES).forEach(function (name) {
        var c = CITIES[name], x = c[0] + 180, y = 90 - c[1];
        s.cities.push({
          dot: mk('circle', { cx: x, cy: y, fill: '#8b7aa8' }, cg),
          text: mk('text', { x: x, y: y, fill: '#5b4d75', 'font-family': 'system-ui, sans-serif', 'font-weight': 600 }, cg),
          x: x, y: y, name: name,
        });
        s.cities[s.cities.length - 1].text.textContent = name;
      });
      el.appendChild(svg);
      var pin = document.createElement('div');
      pin.className = 'ztl-pin';
      pin.innerHTML = '<svg viewBox="0 0 32 44"><path d="M16 43C16 43 2 26.5 2 16a14 14 0 0 1 28 0c0 10.5-14 27-14 27z" fill="' +
        (d.color || DEFAULTS.color) + '" stroke="#fff" stroke-width="2"/><circle cx="16" cy="16" r="5.5" fill="#fff"/></svg>';
      s.ring = document.createElement('div');
      s.ring.className = 'ztl-ring';
      s.name = document.createElement('div');
      s.name.className = 'ztl-name';
      s.hud = document.createElement('div');
      s.hud.className = 'ztl-hud';
      [s.ring, pin, s.name, s.hud].forEach(function (n) { n.setAttribute('aria-hidden', 'true'); el.appendChild(n); });
      s.pin = pin;
      el.setAttribute('role', 'img');
      if (!el.getAttribute('aria-label')) {
        el.setAttribute('aria-label', 'Map zooming in on ' + s.places.map(function (p) { return p[2] || 'a place'; }).join(', then '));
      }
      s.view = { x: 180, y: 90 - 12, w: 360 };
      render(el);
      new ResizeObserver(function () { render(el); }).observe(el);
    }

    function worldView(el) { return { x: 180, y: 78, w: 360 }; }

    function render(el) {
      var s = el._ztl, v = s.view, w = el.clientWidth || 300, h = el.clientHeight || 170;
      var vw = Math.max(v.w, 1), vh = vw * h / w;
      s.svg.setAttribute('viewBox', (v.x - vw / 2) + ' ' + (v.y - vh / 2) + ' ' + vw + ' ' + vh);
      var u = vw / w, zoom = 360 / vw;
      var show = Math.max(0, Math.min(1, (zoom - 3) / 3));
      // Names that would overlap one already placed are hidden. The place
      // being visited claims its space first.
      var boxes = [], x0 = v.x - vw / 2, y0 = v.y - vh / 2;
      var order = s.hideName ? [s.hideName].concat(s.cities) : s.cities;
      order.forEach(function (c, i) {
        if (i && c === s.hideName) return;
        var bx = (c.x - x0) / u, by = (c.y - y0) / u, bw = c.name.length * 6.4 + 10;
        var box = [bx - 4, by - 9, bx + bw, by + 9];
        var free = box[2] < w && box[0] > 0 && boxes.every(function (o) { return box[2] < o[0] || box[0] > o[2] || box[3] < o[1] || box[1] > o[3]; });
        if (free) boxes.push(box);
        c.dot.setAttribute('r', 2.6 * u);
        c.dot.setAttribute('opacity', 0.35 + show * 0.65);
        c.text.setAttribute('font-size', 11 * u);
        c.text.setAttribute('x', c.x + 5 * u);
        c.text.setAttribute('y', c.y + 4 * u);
        c.text.setAttribute('opacity', c === s.hideName || !free ? 0 : show);
      });
      var lat = 90 - v.y, lon = v.x - 180;
      s.hud.textContent = Math.abs(lat).toFixed(2) + '°' + (lat >= 0 ? 'N' : 'S') + '  ' + Math.abs(lon).toFixed(2) + '°' + (lon >= 0 ? 'E' : 'W') + '  ·  ' + zoom.toFixed(1) + '×';
    }

    // Fly from the current view to another. Width changes exponentially;
    // the centre moves in step with the zoom, so the target stays steady.
    function flyTo(el, to, dur, done) {
      var s = el._ztl, from = { x: s.view.x, y: s.view.y, w: s.view.w };
      var st = { t: 0 };
      anime.animate(st, {
        t: 1, duration: dur, ease: 'inOut(3)',
        onUpdate: function () {
          var t = st.t, w = Math.exp(Math.log(from.w) + (Math.log(to.w) - Math.log(from.w)) * t);
          var k = Math.abs(from.w - to.w) > 0.01 ? (from.w - w) / (from.w - to.w) : t;
          s.view = { x: from.x + (to.x - from.x) * k, y: from.y + (to.y - from.y) * k, w: w };
          render(el);
        },
        onComplete: done,
      });
    }

    function arrive(el, p) {
      var s = el._ztl;
      s.hideName = s.cities.filter(function (c) { return c.name === p[2]; })[0] || null;
      render(el);
      s.name.textContent = p[2] || (p[1].toFixed(2) + ', ' + p[0].toFixed(2));
      anime.animate(s.pin, { opacity: [0, 1], y: [-40, 0], duration: 520, ease: 'outBounce' });
      anime.animate(s.ring, { opacity: [0.9, 0], scale: [0.3, 3], duration: 1100, delay: 380, ease: 'out(3)' });
      anime.animate(s.name, { opacity: [0, 1], y: [8, 0], duration: 380, delay: 450, ease: 'outBack(2)' });
    }

    function leave(el) {
      var s = el._ztl;
      anime.animate([s.pin, s.name], { opacity: 0, duration: 250, ease: 'in(2)' });
      s.hideName = null;
    }

    function target(el, p) { return { x: p[0] + 180, y: 90 - p[1], w: 360 / el._ztl.zoom }; }

    function go(el, i) {
      var s = el._ztl, d = el.dataset, dur = +d.duration || DEFAULTS.duration, hold = +d.hold || DEFAULTS.hold;
      if (i >= s.places.length) {
        if (d.loop === undefined) return;
        leave(el);
        flyTo(el, worldView(el), dur * 0.8, function () { setTimeout(function () { go(el, 0); }, 900); });
        return;
      }
      var to = target(el, s.places[i]);
      function dive(ms) {
        flyTo(el, to, ms, function () {
          arrive(el, s.places[i]);
          setTimeout(function () { go(el, i + 1); }, hold + 600);
        });
      }
      if (i === 0) return dive(dur);
      // Between places: pull back out far enough to see both, then dive in.
      leave(el);
      var dx = to.x - s.view.x, dy = to.y - s.view.y, dist = Math.sqrt(dx * dx + dy * dy);
      var mid = { x: (to.x + s.view.x) / 2, y: (to.y + s.view.y) / 2, w: Math.min(360, Math.max(to.w * 2, dist * 1.6)) };
      flyTo(el, mid, dur * 0.5, function () { dive(dur * 0.6); });
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          setTimeout(function () { go(e.target, 0); }, 500);
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-zoom-to-location]').forEach(function (el) {
        if (el._ztl) return;
        setup(el);
        if (reduced()) {
          var s = el._ztl;
          s.view = target(el, s.places[0]);
          render(el);
          s.pin.style.opacity = 1;
          s.name.style.opacity = 1;
          s.name.textContent = s.places[0][2];
          return;
        }
        io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-zoom-to-location data-places="Paris"></div>

<!-- or a tour: -->
<div data-zoom-to-location data-places="London, New York, Tokyo" data-loop></div>