> ## Documentation Index
> Fetch the complete documentation index at: /llms.txt
> Use this file to discover all available pages before exploring further.

# US Bubble Map

> Animated US bubble map with proportional city markers, value callouts, and connection lines — composable with us-map

# US Bubble Map

Animated US bubble map with proportional city markers, value callouts, and connection lines — composable with us-map

**US Bubble Map**

```html us-map-bubble.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=1920, height=1080" />
    <title>US Bubble Map</title>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=block"
      rel="stylesheet"
    />
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/topojson-client@3.1.0/dist/topojson-client.min.js"></script>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      html,
      body {
        margin: 0;
        width: 1920px;
        height: 1080px;
        overflow: hidden;
        background-color: #0f172a;
      }
    </style>
  </head>
  <body>
    <div
      id="us-map-bubble"
      data-composition-id="us-map-bubble"
      data-width="1920"
      data-height="1080"
      data-start="0"
      data-duration="12"
    >
      <div class="map-container">
        <div class="header">
          <h1 class="headline">Top 20 US Cities by Population</h1>
          <p class="subtitle">Proportional bubble markers by metropolitan population</p>
        </div>

        <svg class="map-svg" viewBox="0 0 1400 820" preserveAspectRatio="xMidYMid meet">
          <g class="states-group"></g>
          <g class="connections-group"></g>
          <g class="bubbles-group"></g>
          <g class="callouts-group"></g>
        </svg>

        <div class="source">Source: U.S. Census Bureau, 2024</div>
      </div>

      <style>
        #us-map-bubble {
          width: 1920px;
          height: 1080px;
          background: linear-gradient(145deg, #0f172a 0%, #1e293b 100%);
          font-family: "Inter", sans-serif;
          color: #e2e8f0;
          display: flex;
          justify-content: center;
          align-items: center;
          overflow: hidden;
        }

        #us-map-bubble .map-container {
          width: 1700px;
          height: 950px;
          display: flex;
          flex-direction: column;
          align-items: center;
          position: relative;
        }

        #us-map-bubble .header {
          text-align: center;
          margin-bottom: 20px;
        }

        #us-map-bubble .headline {
          font-size: 52px;
          font-weight: 700;
          letter-spacing: -0.02em;
          color: #f8fafc;
          clip-path: inset(0 100% 0 0);
        }

        #us-map-bubble .subtitle {
          font-size: 20px;
          font-weight: 300;
          color: #94a3b8;
          margin-top: 8px;
          opacity: 0;
        }

        #us-map-bubble .map-svg {
          width: 1500px;
          height: 750px;
          overflow: visible;
        }

        #us-map-bubble .state-path {
          fill: #1e293b;
          stroke: #334155;
          stroke-width: 0.5;
          opacity: 0;
        }

        #us-map-bubble .city-bubble {
          fill: rgba(59, 130, 246, 0.6);
          stroke: #ffffff;
          stroke-width: 1;
          transform-origin: center center;
        }

        #us-map-bubble .city-pulse {
          fill: none;
          stroke: rgba(59, 130, 246, 0.4);
          stroke-width: 2;
          opacity: 0;
        }

        #us-map-bubble .callout-group {
          opacity: 0;
        }

        #us-map-bubble .callout-line {
          stroke: rgba(148, 163, 184, 0.5);
          stroke-width: 1;
          fill: none;
        }

        #us-map-bubble .callout-name {
          font-size: 13px;
          font-weight: 600;
          fill: #f8fafc;
        }

        #us-map-bubble .callout-value {
          font-size: 11px;
          font-weight: 400;
          fill: #94a3b8;
        }

        #us-map-bubble .connection-line {
          stroke: rgba(59, 130, 246, 0.3);
          stroke-width: 1;
          fill: none;
          stroke-dasharray: 6 4;
        }

        #us-map-bubble .source {
          position: absolute;
          bottom: 0;
          right: 40px;
          font-size: 12px;
          color: #475569;
          opacity: 0;
        }
      </style>

      <script>
        (function () {
          var US_TOPO_URL = "https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json";

          var cities = [
            { name: "New York", lat: 40.7128, lon: -74.006, pop: 8300000 },
            { name: "Los Angeles", lat: 34.0522, lon: -118.2437, pop: 3900000 },
            { name: "Chicago", lat: 41.8781, lon: -87.6298, pop: 2700000 },
            { name: "Houston", lat: 29.7604, lon: -95.3698, pop: 2300000 },
            { name: "Phoenix", lat: 33.4484, lon: -112.074, pop: 1600000 },
            { name: "Philadelphia", lat: 39.9526, lon: -75.1652, pop: 1600000 },
            { name: "San Antonio", lat: 29.4241, lon: -98.4936, pop: 1400000 },
            { name: "San Diego", lat: 32.7157, lon: -117.1611, pop: 1400000 },
            { name: "Dallas", lat: 32.7767, lon: -96.797, pop: 1300000 },
            { name: "Jacksonville", lat: 30.3322, lon: -81.6557, pop: 950000 },
            { name: "Austin", lat: 30.2672, lon: -97.7431, pop: 960000 },
            { name: "San Jose", lat: 37.3382, lon: -121.8863, pop: 1000000 },
            { name: "Fort Worth", lat: 32.7555, lon: -97.3308, pop: 930000 },
            { name: "Columbus", lat: 39.9612, lon: -82.9988, pop: 910000 },
            { name: "Charlotte", lat: 35.2271, lon: -80.8431, pop: 870000 },
            { name: "Indianapolis", lat: 39.7684, lon: -86.1581, pop: 880000 },
            { name: "San Francisco", lat: 37.7749, lon: -122.4194, pop: 870000 },
            { name: "Seattle", lat: 47.6062, lon: -122.3321, pop: 740000 },
            { name: "Denver", lat: 39.7392, lon: -104.9903, pop: 710000 },
            { name: "Nashville", lat: 36.1627, lon: -86.7816, pop: 690000 },
          ];

          var radiusScale = d3.scaleSqrt().domain([500000, 8500000]).range([6, 45]);

          var svg = document.querySelector("#us-map-bubble .map-svg");
          var statesGroup = svg.querySelector(".states-group");
          var bubblesGroup = svg.querySelector(".bubbles-group");
          var calloutsGroup = svg.querySelector(".callouts-group");
          var connectionsGroup = svg.querySelector(".connections-group");
          var tl = gsap.timeline({ paused: true });

          function formatPop(n) {
            if (n >= 1000000) {
              return (n / 1000000).toFixed(1) + "M";
            }
            return (n / 1000).toFixed(0) + "K";
          }

          fetch(US_TOPO_URL)
            .then(function (r) {
              return r.json();
            })
            .then(function (us) {
              var states = topojson.feature(us, us.objects.states).features;
              var projection = d3.geoAlbersUsa().fitSize([1380, 800], {
                type: "FeatureCollection",
                features: states,
              });
              var path = d3.geoPath().projection(projection);

              states.forEach(function (feature) {
                var d = path(feature);
                if (!d) return;

                var el = document.createElementNS("http://www.w3.org/2000/svg", "path");
                el.setAttribute("d", d);
                el.setAttribute("class", "state-path");
                statesGroup.appendChild(el);
              });

              var projectedCities = [];
              cities.forEach(function (city) {
                var coords = projection([city.lon, city.lat]);
                if (!coords) return;
                projectedCities.push({
                  name: city.name,
                  pop: city.pop,
                  x: coords[0],
                  y: coords[1],
                  r: radiusScale(city.pop),
                });
              });

              projectedCities.sort(function (a, b) {
                return b.pop - a.pop;
              });

              var nycCoords = null;
              projectedCities.forEach(function (city) {
                if (city.name === "New York") {
                  nycCoords = { x: city.x, y: city.y };
                }
              });

              var connectionTargets = ["Los Angeles", "Chicago", "Houston", "Phoenix"];
              connectionTargets.forEach(function (targetName) {
                if (!nycCoords) return;
                var target = null;
                projectedCities.forEach(function (c) {
                  if (c.name === targetName) target = c;
                });
                if (!target) return;

                var midX = (nycCoords.x + target.x) / 2;
                var midY = Math.min(nycCoords.y, target.y) - 60;

                var pathD =
                  "M " +
                  nycCoords.x +
                  " " +
                  nycCoords.y +
                  " Q " +
                  midX +
                  " " +
                  midY +
                  " " +
                  target.x +
                  " " +
                  target.y;

                var line = document.createElementNS("http://www.w3.org/2000/svg", "path");
                line.setAttribute("d", pathD);
                line.setAttribute("class", "connection-line");
                connectionsGroup.appendChild(line);

                var length = line.getTotalLength();
                line.style.strokeDasharray = length;
                line.style.strokeDashoffset = length;
              });

              projectedCities.forEach(function (city) {
                var circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
                circle.setAttribute("cx", city.x);
                circle.setAttribute("cy", city.y);
                circle.setAttribute("r", city.r);
                circle.setAttribute("class", "city-bubble");
                circle.setAttribute("data-city", city.name);
                bubblesGroup.appendChild(circle);
              });

              var pulseCircle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
              if (nycCoords) {
                pulseCircle.setAttribute("cx", nycCoords.x);
                pulseCircle.setAttribute("cy", nycCoords.y);
                pulseCircle.setAttribute("r", radiusScale(8300000));
                pulseCircle.setAttribute("class", "city-pulse");
                bubblesGroup.appendChild(pulseCircle);
              }

              var top5 = projectedCities.slice(0, 5);
              top5.forEach(function (city, i) {
                var g = document.createElementNS("http://www.w3.org/2000/svg", "g");
                g.setAttribute("class", "callout-group");

                var offsetX = city.r + 12;
                var offsetY = -8;

                if (city.name === "Los Angeles" || city.name === "San Francisco") {
                  offsetX = -(city.r + 80);
                }

                var calloutLine = document.createElementNS("http://www.w3.org/2000/svg", "line");
                calloutLine.setAttribute("x1", city.x);
                calloutLine.setAttribute("y1", city.y);
                calloutLine.setAttribute("x2", city.x + offsetX);
                calloutLine.setAttribute("y2", city.y + offsetY);
                calloutLine.setAttribute("class", "callout-line");
                g.appendChild(calloutLine);

                var nameText = document.createElementNS("http://www.w3.org/2000/svg", "text");
                nameText.setAttribute("x", city.x + offsetX + (offsetX > 0 ? 6 : -6));
                nameText.setAttribute("y", city.y + offsetY - 2);
                nameText.setAttribute("class", "callout-name");
                if (offsetX < 0) {
                  nameText.setAttribute("text-anchor", "end");
                }
                nameText.textContent = city.name;
                g.appendChild(nameText);

                var valueText = document.createElementNS("http://www.w3.org/2000/svg", "text");
                valueText.setAttribute("x", city.x + offsetX + (offsetX > 0 ? 6 : -6));
                valueText.setAttribute("y", city.y + offsetY + 14);
                valueText.setAttribute("class", "callout-value");
                if (offsetX < 0) {
                  valueText.setAttribute("text-anchor", "end");
                }
                valueText.textContent = formatPop(city.pop) + " residents";
                g.appendChild(valueText);

                calloutsGroup.appendChild(g);
              });

              buildTimeline(projectedCities, pulseCircle);
            });

          function buildTimeline(projectedCities, pulseCircle) {
            var statePaths = statesGroup.querySelectorAll(".state-path");
            var bubbles = bubblesGroup.querySelectorAll(".city-bubble");
            // Own the full transform via GSAP from the start (not CSS), so the
            // scale tween below doesn't clobber a value GSAP never tracked.
            gsap.set(bubbles, { scale: 0 });
            var callouts = calloutsGroup.querySelectorAll(".callout-group");
            var connections = connectionsGroup.querySelectorAll(".connection-line");

            tl.to(
              "#us-map-bubble .headline",
              {
                clipPath: "inset(0 0% 0 0)",
                duration: 1.0,
                ease: "power2.inOut",
              },
              0,
            );

            tl.to(
              "#us-map-bubble .subtitle",
              {
                opacity: 1,
                duration: 0.6,
                ease: "power2.out",
              },
              0.4,
            );

            tl.to(
              statePaths,
              {
                opacity: 0.8,
                duration: 0.6,
                ease: "power2.out",
              },
              0.8,
            );

            tl.to(
              bubbles,
              {
                scale: 1,
                duration: 0.5,
                stagger: {
                  each: 0.08,
                  from: "start",
                },
                ease: "back.out(1.7)",
              },
              1.5,
            );

            tl.set(callouts, { x: 20 }, 3.9);

            tl.to(
              callouts,
              {
                opacity: 1,
                x: 0,
                duration: 0.6,
                stagger: 0.3,
                ease: "power2.out",
              },
              4.0,
            );

            connections.forEach(function (line) {
              tl.to(
                line,
                {
                  strokeDashoffset: 0,
                  duration: 1.2,
                  ease: "power2.inOut",
                },
                6.0,
              );
            });

            if (pulseCircle) {
              tl.to(
                pulseCircle,
                {
                  opacity: 1,
                  duration: 0.01,
                },
                8.0,
              );

              tl.fromTo(
                pulseCircle,
                {
                  attr: { r: radiusScale(8300000) },
                  opacity: 0.6,
                },
                {
                  attr: { r: radiusScale(8300000) + 30 },
                  opacity: 0,
                  duration: 1.5,
                  repeat: 2,
                  ease: "power2.out",
                },
                8.0,
              );
            }

            tl.to(
              "#us-map-bubble .source",
              {
                opacity: 1,
                duration: 0.5,
                ease: "power2.out",
              },
              5.5,
            );

            window.__timelines = window.__timelines || {};
            window.__timelines["us-map-bubble"] = tl;
          }
        })();
      </script>
    </div>
  </body>
</html>
```

Copy npx hyperframes add us-map-bubble to the clipboard

That writes one file: `compositions/us-map-bubble.html`.

## Add it to your video

It runs for 12 seconds at 1920×1080. Paste this into your composition:

```html index.html
<div
  data-composition-id="us-map-bubble"
  data-composition-src="compositions/us-map-bubble.html"
  data-start="0"
  data-duration="12"
  data-track-index="1"
  data-width="1920"
  data-height="1080"
></div>
```

Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes.md) for the rest.

{/* hf:generated-footer */}

Tagged `data` `map` `geography` `usa` `bubble` `cities`.

## Related topics

* [Browse the complete Catalog](/catalog.md)
* [Add assets and Catalog items in the browser editor](/studio/assets-and-blocks.md)
* [Put blocks together into one video](/concepts/compositions.md)
