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.
Styles
markerStyle decides how a marker is drawn:
| Value | Behaviour |
|---|---|
"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.
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" },
],
});
| Field | Meaning |
|---|---|
image | URL, HTMLImageElement, HTMLCanvasElement or ImageBitmap |
imageSize | Radius 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 field | Meaning |
|---|---|
x, y | Screen position in CSS pixels |
depth | 0…1 foreshortening on the globe; always 1 on a map |
scale | The current markerScale |
theme | The resolved theme object |
max | Largest count in the set, for relative sizing |
globe | The 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
- Clustering: for thousands of points
- Data in: load markers from a CSV
- Overlays: labels, counters and callouts