Skip to main content

Markers

A marker needs two things: lat and lon. Everything else shapes how it looks or comes back to your callbacks untouched.

{
lat: number; // required, -90…90
lon: number; // required, -180…180
count?: number; // relative weight: bigger count, bigger marker
emoji?: string; // drawn inside a bubble
image?: string; // logo or avatar, cropped to a circle
live?: boolean; // pulsing ring
color?: string; // overrides the theme colour
size?: number; // base dot radius, default 3.4
date?: string; // used by the timeline
...anything // handed straight back to onHover / onClick
}

Weight

count scales a marker relative to the largest one in the set, so a marker with count: 100 alongside one with count: 1 reads as clearly bigger without swamping it.

Loading globe…

Styles

markerStyle decides how a marker is drawn:

ValueBehaviour
"auto" (default)A bubble when the marker has an emoji or count > 1, otherwise a dot
"dot"Always a plain filled circle
"bubble"Always a bubble

An image always wins, and clusters are always bubbles.

Emoji

Emoji can distinguish marker categories without loading images.

Loading globe…

Logos and avatars

Set image to a URL or anything drawImage accepts. It is cropped to a circle, ringed in the marker colour, and given a count badge when count > 1.

createGlobe(canvas, {
markers: [
{ lat: 37.77, lon: -122.42, image: "/logos/acme.svg", imageSize: 22, label: "Acme" },
{ lat: 52.52, lon: 13.4, image: "/team/priya.jpg", label: "Priya" },
],
});
FieldMeaning
imageURL, HTMLImageElement, HTMLCanvasElement or ImageBitmap
imageSizeRadius in px before depth and scale. Default 19

:::caution Cross-origin images Images are drawn with drawImage, so a cross-origin URL needs permissive CORS headers. Same-origin paths and data URLs always work. :::

Live pulses

live: true adds an expanding ring. Use it for "active right now", not for everything: a map where every marker pulses reads as noise.

Pulses stop automatically when the visitor prefers reduced motion.

Hover and click

createGlobe(canvas, {
markers,
onHover: (marker, position) => {
// marker is null when the pointer leaves
// position is { x, y } in CSS pixels
},
onClick: (marker) => globe.flyTo(marker.lon, marker.lat, { zoom: 3 }),
});

Hit testing walks front-to-back, so overlapping markers resolve to the one on top.

Tooltips

tooltip: true uses a built-in formatter. Pass a function for your own text:

tooltip: (target, kind) => {
if (kind === "cluster") return `${target.count} people nearby`;
if (kind === "country") return target.name;
return `${target.city}: ${target.count}`;
};

:::info Tooltips are text, never HTML The returned string is set with textContent, so a marker label pulled from user data cannot inject markup. :::

Drawing markers yourself

renderMarker hands you the context and the resolved screen position. Return the hit radius.

createGlobe(canvas, {
renderMarker(ctx, marker, { x, y, depth, theme }) {
const r = 8 * depth;
ctx.fillStyle = marker.color || theme.marker;
ctx.fillRect(x - r, y - r, r * 2, r * 2);
return r;
},
});
Info fieldMeaning
x, yScreen position in CSS pixels
depth0…1 foreshortening on the globe; always 1 on a map
scaleThe current markerScale
themeThe resolved theme object
maxLargest count in the set, for relative sizing
globeThe instance

Updating

globe.setMarkers(next); // replaces the whole set

setMarkers recomputes the weight ceiling and clears the keyboard focus. It does not disturb the viewer pin, which lives outside the marker array.

Next