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 requesttype: 'hexbin' · the same 20,000 pointsSynthetic 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.
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.
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.
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.
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 manyaggregate: '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 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.
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.