# WorldGlobe

> An interactive d3-geo globe on canvas with avatar markers that merge into heat-coloured count bubbles.

## Facts

- **Product**: Max
- **Family**: Data display
- **Global**: `window.DigitalCrew.WorldGlobe`
- **Source**: `max-agent: src/components/shared/world-globe/world-globe.tsx`
- **Import in the app**: `import { WorldGlobe } from "@/components/shared/world-globe/world-globe";`
- **Live preview**: /design/components/world-globe/preview.html
- **Page**: /design/components/world-globe

## Guidelines

**Use it for** the Map view of Saved › People and Saved › Organizations, next to [`ListMapToggle`](/design/components/list-map-toggle.md).

**What you provide**:
- `features`: the `features` array of `world-countries-110m.geojson`.
- `points: { id, name, imageUrl | null, city, country, lat, lng }[]`. Without an image, a marker shows initials.
- `entityLabel: { singular, plural }` and `getHref(point)`.
- Optional `renderDetail(point)`, which opens a `sm:max-w-4xl` dialog instead of navigating.
- `className`: give it a height; Max uses a parent of `h-[calc(100vh-260px)] min-h-[420px]`.

**Anatomy**:
- Single markers are 34px avatars with `ring-2 ring-background`.
- A bubble is 26 + 6·√count px, clamped between 30 and 58px. Its colour runs from blue-600 through violet-600 and orange-600 to red-600, log-scaled against the busiest bubble on screen.
- A legend sits top-left and 32px zoom and reset buttons sit bottom-right. A bubble opens a 288px list card.

**Behaviour**: the view starts centred on the points at zoom 1.4 (range 0.85–9). Dragging spins the globe after 4px of movement, and the wheel zooms. Reset animates back over 600ms with ease-in-out. Colours follow next-themes' `resolvedTheme`: in light, ocean `#eef2f6` and land `#cbd3dc`; in dark, ocean `#12161c` and land `#2b313a`. The preview wraps it in `DC.PageThemeProvider` so it follows the page theme; in the app, the root `ThemeProvider` does this.

**Don't** pass points without a location. Filter them out and report how many are missing, as the people map does.

## API

```ts
/** Domain-agnostic marker: an organization, a person, … */
export interface WorldGlobePoint {
    id: string;
    name: string;
    /** Logo or profile photo; initials fallback when null or broken. */
    imageUrl: string | null;
    city: string | null;
    country: string | null;
    lat: number;
    lng: number;
}
export interface WorldGlobeProps {
    features: WorldFeature[];
    points: WorldGlobePoint[];
    /** Noun used in tooltips and the preview card, e.g. "organization". */
    entityLabel: {
        singular: string;
        plural: string;
    };
    /** Detail-page href for a point (the "open full page" link in the popup). */
    getHref: (point: WorldGlobePoint) => string;
    /**
     * Body of the big detail popup for a point. When provided, clicking a single
     * avatar (or a row in a cluster's list) opens a modal rendering this instead
     * of navigating to {@link getHref}. Omit to keep plain navigation.
     */
    renderDetail?: (point: WorldGlobePoint) => ReactNode;
    className?: string;
}
export declare function WorldGlobe({ features, points, entityLabel, getHref, renderDetail, className, }: WorldGlobeProps): JSX.Element;
```

## Example

```html
<div id="root" class="p-6"></div>
<script>
(function () {
  var h = React.createElement, DC = window.DigitalCrew, I = DC.Icons;
  // WorldGlobePoint[]: geocoded prospects. imageUrl is null, so markers show initials.
  var PEOPLE = [
    ["Claire Dubois", "Lyon", "France", 45.764, 4.8357],
    ["Lucas Moreau", "Paris", "France", 48.8566, 2.3522],
    ["Camille Laurent", "Paris", "France", 48.8666, 2.3333],
    ["Hugo Martin", "Paris", "France", 48.8466, 2.3622],
    ["Jonas Weber", "Berlin", "Germany", 52.52, 13.405],
    ["Lena Fischer", "Munich", "Germany", 48.1351, 11.582],
    ["Tom Becker", "Hamburg", "Germany", 53.5511, 9.9937],
    ["Sofia Rossi", "Milan", "Italy", 45.4642, 9.19],
    ["Mateo García", "Madrid", "Spain", 40.4168, -3.7038],
    ["Emma de Vries", "Amsterdam", "Netherlands", 52.3676, 4.9041],
    ["Oliver Bennett", "London", "United Kingdom", 51.5074, -0.1278],
    ["Amelia Hart", "London", "United Kingdom", 51.5174, -0.1178],
    ["Freya Nilsson", "Stockholm", "Sweden", 59.3293, 18.0686],
    ["Aoife Byrne", "Dublin", "Ireland", 53.3498, -6.2603],
    ["Priya Nair", "New York", "United States", 40.7128, -74.006],
    ["Ethan Brooks", "New York", "United States", 40.7228, -73.996],
    ["Maya Chen", "San Francisco", "United States", 37.7749, -122.4194],
    ["Daniel Ortiz", "Austin", "United States", 30.2672, -97.7431],
    ["Rachel Kim", "Boston", "United States", 42.3601, -71.0589],
    ["Noah Williams", "Chicago", "United States", 41.8781, -87.6298],
    ["Chloé Tremblay", "Montréal", "Canada", 45.5017, -73.5673]
  ];
  var POINTS = PEOPLE.map(function (p, i) {
    return { id: "prospect-" + (i + 1), name: p[0], imageUrl: null, city: p[1], country: p[2], lat: p[3], lng: p[4] };
  });
  function Detail(props) {
    var p = props.point;
    return h("div", { className: "space-y-1 text-sm" },
      h("p", { className: "font-medium" }, p.name),
      h("p", { className: "text-muted-foreground" }, [p.city, p.country].filter(Boolean).join(", ")));
  }
  function App() {
    var s = React.useState(null), features = s[0], setFeatures = s[1];
    var e = React.useState(false), failed = e[0], setFailed = e[1];
    React.useEffect(function () {
      fetch("../../assets/Geo/world-countries-110m.geojson")
        .then(function (r) { if (!r.ok) throw new Error("geo"); return r.json(); })
        .then(function (geo) { setFeatures(geo.features); })
        .catch(function () { setFailed(true); });
    }, []);
    return h("div", { className: "relative h-[480px] overflow-hidden rounded-md border" },
      failed ? h("div", { className: "flex h-full items-center justify-center text-sm text-muted-foreground" }, "Could not load the world map.")
      : !features ? h("div", { className: "flex h-full items-center justify-center gap-2 bg-muted text-sm text-muted-foreground" },
          h(I.Loader2, { className: "h-5 w-5 animate-spin" }), "Loading prospects…")
      : h(DC.WorldGlobe, {
          features: features, points: POINTS, className: "h-full",
          entityLabel: { singular: "person", plural: "people" },
          getHref: function (p) { return "/prospects/" + p.id; },
          renderDetail: function (p) { return h(Detail, { point: p }); }
        }));
  }
  ReactDOM.createRoot(document.getElementById("root")).render(h(DC.PageThemeProvider, null, h(DC.TooltipProvider, { delayDuration: 80 }, h(App))));
})();
</script>
```

## More in Data display

- [AgentFace](/design/components/agent-face.md): A Digital Worker's face: its approved portrait, or its initials on its accent tile.
- [AnimateDigits](/design/components/animate-digits.md): A number whose digits spring in and out as it changes (motion), direction following the change.
- [Avatar](/design/components/avatar.md): A round image with an initials fallback (Radix Avatar), 32px by default.
- [CampaignStatusBadge](/design/components/campaign-status-badge.md): The campaign lifecycle pill: Draft, Active, Paused, Stopped, Completed, Archived.
- [ChannelBadge](/design/components/channel-badge.md): Marks which channel a conversation or step runs on: email (amber), LinkedIn (sky), WhatsApp (emerald).
- [EmailVerificationBadge](/design/components/email-verification-badge.md): The deliverability verdict for an email address, with an icon and an optional score.
- [HeroMetric](/design/components/hero-metric.md): One big number with its previous-period comparison, a trend pill and a pacing bar.
- [KpiCard](/design/components/kpi-card.md): A KPI tile — label, value, trend — and `KpiSection`, the glass panel that groups them.
- [MaxIconSet](/design/components/max-icon-set.md): Max's own SVG icons, exported as `DC.MaxIcons`: integration and channel logos, message delivery states, and the appearance-picker tiles.
- [MetricTile](/design/components/metric-tile.md): A compact glass tile for a scored metric: label, value toned by score band, hint.
- [ProspectAvatar](/design/components/prospect-avatar.md): A prospect's photo, or a gradient disc hashed from their name, with an optional unread halo.
- [Snippet](/design/components/snippet.md): A monospace command block with a copy button whose icon turns into a check after copying.
- [TimeLens](/design/components/time-lens.md): Max's agenda: hour marks, ticks and tasks in one scrolling column, read through a fixed lens that shows the time under it.
