Twenty thousand points, and what each way of drawing them says

A hexbin is what to reach for when there are more points than pixels. Both maps below hold the same 20,000 points. The left one draws every one of them and gives you the shape of the data without its magnitude: you can see there are twelve clusters and roughly where they are, and you cannot rank them or read a number off any of them, because overlapping marks stop counting once they overlap. The right one lays a lattice over the projection and colours each cell by how many landed in it, so the same twelve clusters arrive with a legend that says which is which.

The left one is behind a button, and that is the second half of the same point. Drawing every point means one SVG group, one shape and one hit target per point, so the left map costs around sixty thousand DOM nodes against the right one's few hundred, and the numbers below are measured in your browser as you click rather than quoted from ours. The boundaries are already loaded by then, so what you are waiting for is the drawing.

type: 'marker' · 20,000 marks, on request
Not drawn yet. Every point gets its own group, shape and hit target, so this half is about sixty thousand DOM nodes and it is worth feeling that rather than being told it.
type: 'hexbin' · the same 20,000 points

Synthetic points, real anchors. The cluster centres are the coordinates of twelve US metropolitan areas, which are facts; the scatter around them is generated from a hash of the point index, so the page looks identical on every load. It is not a dataset and no figure here claims otherwise.

Not the same hexagons as the other page

Two features in this library draw hexagons and they have nothing to do with each other, which is worth saying plainly because the word "hex" is doing double duty.

A hex tile layout (layout: 'hex')

A cartogram. One cell per region, placed by hand, and the hexagon is a container for that region's identity: this cell is Texas. It replaces the boundaries, the cell count is fixed at however many regions there are, and the whole point is that every region gets equal room.

A hexbin series (type: 'hexbin')

A density estimate. No regions in it at all: the cells are a lattice laid over the projection, and each reports what landed inside it. It ignores the boundaries rather than replacing them, so it sits on top of a basemap, and the cell count is whatever the data and the radius work out to.

The colour is a choice, and the aggregate is the choice

aggregate: 'count' is the default and needs no value field at all, which is right because "where are these things" is the question that brings anyone here. Give the points a value and the same lattice reports its mean instead, and the two maps then say different things: the left reads as the twelve clusters the points were drawn around, and the right reads as a smooth field across the continent that owes nothing to how many points are anywhere. Same cells, same data, two questions.

aggregate: 'count' · how many
aggregate: 'mean', minCount: 5 · how much, on average

minCount is doing real work on the right. A mean over one point is that point, and it shouts as loudly as a mean over a hundred; raising the floor to five is what keeps the average from being decided by its thinnest cells. Rendering with debug on says so out loud if you forget.

The lattice belongs to the reader, not to the data

The radius is in screen pixels, so the cells stay the size you chose and the lattice refines as the reader zooms rather than magnifying. That is the difference between binning and just drawing bigger hexagons: the resolution follows whoever is looking. Cells are rebuilt at quantized zoom levels, which is the same policy point clustering uses, so a pan never re-bins and a smooth zoom crosses a boundary a handful of times rather than sixty times a second.

1.0x · 0 cells

Watch the legend as well as the map. Smaller cells hold fewer points, so the class breaks move down with them: the same colour means a different number at a different zoom, and the legend says so rather than quietly lying. Pass scale: { domain: [...] } to pin the classes instead, which is what you want when two maps have to be read against each other, and what you do not want as a default: fixed classes leave every cell in the palest one two levels in.