Data display
WorldGlobe
An interactive d3-geo globe on canvas with avatar markers that merge into heat-coloured count bubbles.
Guidelines#
Use it for the Map view of Saved › People and Saved › Organizations, next to ListMapToggle.
What you provide:
features: thefeaturesarray ofworld-countries-110m.geojson.points: { id, name, imageUrl | null, city, country, lat, lng }[]. Without an image, a marker shows initials.entityLabel: { singular, plural }andgetHref(point).- Optional
renderDetail(point), which opens asm:max-w-4xldialog instead of navigating. className: give it a height; Max uses a parent ofh-[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.
Facts#
| Group | Data display |
|---|---|
| Product | Max |
| 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"; |
API#
TypeScript declarations emitted from src/components/shared/world-globe/world-globe.tsx.
/** 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#
The code of the preview above: plain React.createElement against window.DigitalCrew, with realistic data.
<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>