Skip to content
Open

1.0? #19

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
dist/
node_modules
npm-debug.log
test/output
48 changes: 43 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Hexagonal binning is useful for aggregating data into a coarser representation f
If you use NPM, `npm install d3-hexbin`. Otherwise, download the [latest release](https://github.com/d3/d3-hexbin/releases/latest). You can also load directly from [d3js.org](https://d3js.org), either as a [standalone library](https://d3js.org/d3-hexbin.v0.2.min.js). AMD, CommonJS, and vanilla environments are supported. In vanilla, a `d3_hexbin` global is exported:
Copy link
Copy Markdown
Member Author

@Fil Fil Feb 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If you use NPM, `npm install d3-hexbin`. Otherwise, download the [latest release](https://github.com/d3/d3-hexbin/releases/latest). You can also load directly from [d3js.org](https://d3js.org), either as a [standalone library](https://d3js.org/d3-hexbin.v0.2.min.js). AMD, CommonJS, and vanilla environments are supported. In vanilla, a `d3_hexbin` global is exported:
If you use NPM, `npm install d3-hexbin`. Otherwise, download the [latest release](https://github.com/d3/d3-hexbin/releases/latest). You can also load directly from [d3js.org](https://d3js.org), either as a [standalone library](https://d3js.org/d3-hexbin.v1.min.js). AMD, CommonJS, and vanilla environments are supported. In vanilla, a `d3_hexbin` global is exported:

(I think we'll want to update the distribution pattern.)


```html
<script src="https://d3js.org/d3-hexbin.v0.2.min.js"></script>
<script src="https://d3js.org/d3-hexbin.v1.min.js"></script>
<script>

const hexbin = d3.hexbin();
Expand Down Expand Up @@ -46,7 +46,7 @@ You could display a hexagon for each non-empty bin as follows:
svg.selectAll("path")
.data(hexbin(points))
.enter().append("path")
.attr("d", d => `M${d.x},${d.y}${hexbin.hexagon()}`);
.attr("d", (d) => hexbin.hexagon([d.x, d.y]));
```

Alternatively, using a transform:
Expand All @@ -61,9 +61,25 @@ svg.selectAll("path")

This method ignores the hexbin’s [extent](#hexbin_extent); it may return bins outside the extent if necessary to contain the specified points.

<a name="hexbin_hexagon" href="#hexbin_hexagon">#</a> <i>hexbin</i>.<b>hexagon</b>([<i>radius</i>])
<a name="hexbin_add" href="#hexbin_add">#</a> <i>hexbin</i>.<b>add</b>(<i>point</i>)

Returns the SVG path string for the hexagon centered at the origin ⟨0,0⟩. The path string is defined with relative coordinates such that you can easily translate the hexagon to the desired position. If *radius* is not specified, the hexbin’s [current radius](#hexbin_radius) is used. If *radius* is specified, a hexagon with the specified radius is returned; this is useful for area-encoded bivariate hexbins.
Adds the *point* and returns the hexbin generator.

<a name="hexbin_addAll" href="#hexbin_addAll">#</a> <i>hexbin</i>.<b>addAll</b>(<i>points</i>)

Adds the *points* and returns the hexbin generator.

<a name="hexbin_remove" href="#hexbin_remove">#</a> <i>hexbin</i>.<b>remove</b>(<i>point</i>)

Removes the *point*, and returns the hexbin generator. Empty bins are pruned.

<a name="hexbin_removeAll" href="#hexbin_removeAll">#</a> <i>hexbin</i>.<b>removeAll</b>(<i>points</i>)

Removes all the *points* and returns the hexbin generator. Empty bins are pruned.

<a name="hexbin_hexagon" href="#hexbin_hexagon">#</a> <i>hexbin</i>.<b>hexagon</b>([<i>radius</i>][, <i>translate</i>])

Returns the SVG path string for the hexagon centered at the origin ⟨0,0⟩. If *translate* is specified, the path is translated. Otherwise, the path string is defined with relative coordinates. If *radius* is not specified, the hexbin’s [current radius](#hexbin_radius) is used. If *radius* is specified, a hexagon with the specified radius is returned; this is useful for area-encoded bivariate hexbins.

<a name="hexbin_centers" href="#hexbin_centers">#</a> <i>hexbin</i>.<b>centers</b>()

Expand Down Expand Up @@ -97,9 +113,17 @@ function y(d) {

The *y*-coordinate accessor is used by [*hexbin*](#_hexbin) to compute the *y*-coordinate of each point. The default value assumes each point is specified as a two-element array of numbers [*x*, *y*].

<a name="hexbin_angle" href="#hexbin_angle">#</a> <i>hexbin</i>.<b>angle</b>([<i>angle</i>])

If *angle* is specified, sets the angle of the hexagonal grid to the specified number, in degrees. If *angle* is not specified, returns the current angle, which defaults to 0 (pointy-topped hexagons).

<a name="hexbin_translate" href="#hexbin_translate">#</a> <i>hexbin</i>.<b>translate</b>([<i>translate</i>])

If *translate* is specified, translates the hexagonal grid to the specified value [tx, ty]. If *translate* is not specified, returns the current translate, which defaults to [0, 0].

<a name="hexbin_radius" href="#hexbin_radius">#</a> <i>hexbin</i>.<b>radius</b>([<i>radius</i>])

If *radius* is specified, sets the radius of the hexagon to the specified number. If *radius* is not specified, returns the current radius, which defaults to 1. The hexagons are pointy-topped (rather than flat-topped); the width of each hexagon is *radius* × 2 × sin(π / 3) and the height of each hexagon is *radius* × 3 / 2.
If *radius* is specified, sets the radius of the hexagon to the specified number. If *radius* is not specified, returns the current radius, which defaults to 1. The width of each hexagon is *radius* × 2 × sin(π / 3) and the height of each hexagon is *radius* × 3 / 2.

<a name="hexbin_extent" href="#hexbin_extent">#</a> hexbin.<b>extent</b>([<i>extent</i>])

Expand All @@ -113,3 +137,17 @@ If *size* is specified, sets the [extent](#hexbin_extent) to the specified bound
hexbin.extent([[0, 0], [width, height]]);
hexbin.size([width, height]);
```

<a href="#hexbin_context" name="hexbin_context">#</a> <i>hexbin</i>.<b>context</b>([<i>context</i>]) [<>](https://github.com/d3/d3-hexbin/blob/master/src/hexbin.js "Source")

If *context* is specified, sets the current render context and returns the hexbin. If the *context* is null, hexbin.mesh and hexbin.hexagon will return SVG path strings; if the context is non-null, hexbin.mesh and hexbin.hexagon will instead call methods on the specified context to render geometry. The context must implement the following subset of the [CanvasRenderingContext2D API](https://www.w3.org/TR/2dcontext/#canvasrenderingcontext2d):

* *context*.moveTo(*x*, *y*)
* *context*.lineTo(*x*, *y*)

If a *context* is not specified, returns the current render context which defaults to null.


<a href="#hexbin_bin" name="hexbin_bin">#</a> <i>hexbin</i>.<b>bin</b>(<i>point</i>) [<>](https://github.com/d3/d3-hexbin/blob/master/src/hexbin.js "Source")

Returns the bin that would contain the point if we added it. If there is no such bin, returns an empty array with properties *x* and *y*. That bin is not guaranteed to keep in sync with data additions and removals, or changes of parameters such as radius, angle and translate.
Binary file added img/hexagons.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@
"scripts": {
"pretest": "rollup -c",
"test": "tape 'test/**/*-test.js' && eslint src test",
"prepublishOnly": "rm -rf dist && yarn test",
"prepublishOnly": "rm -rf dist && yarn test && mkdir -p test/output && test/compare-images",
"postpublish": "git push && git push --tags && cd ../d3.github.com && git pull && cp ../${npm_package_name}/dist/${npm_package_name}.js ${npm_package_name}.v${npm_package_version%%.*}.js && cp ../${npm_package_name}/dist/${npm_package_name}.min.js ${npm_package_name}.v${npm_package_version%%.*}.min.js && git add ${npm_package_name}.v${npm_package_version%%.*}.js ${npm_package_name}.v${npm_package_version%%.*}.min.js && git commit -m \"${npm_package_name} ${npm_package_version}\" && git push && cd - && zip -j dist/${npm_package_name}.zip -- LICENSE README.md dist/${npm_package_name}.js dist/${npm_package_name}.min.js"
},
"sideEffects": false,
"devDependencies": {
"canvas": "1 - 2",
"eslint": "6",
"rollup": "1",
"rollup-plugin-terser": "5",
Expand Down
233 changes: 190 additions & 43 deletions src/hexbin.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,74 +18,180 @@ export default function() {
y = pointY,
r,
dx,
dy;
dy,
tx = 0,
ty = 0,
angle = 0,
ca = 1,
sa = 0,
context = null,
binsById = {},
bins = [];

// from pixels to grid
function transform(x, y) {
x -= tx;
y -= ty;
if (ca === 1) return [x, y];
return [x * ca - y * sa, x * sa + y * ca];
}

// from grid to pixels
function untransform(x, y) {
if (ca === 1) return [x + tx, y + ty];
return [x * ca + y * sa + tx, - x * sa + y * ca + ty];
}

function hexbin(points) {
var binsById = {}, bins = [], i, n = points.length;
if (points) {
binsById = {};
bins.splice(0, bins.length);
addAll(points);
}
return bins;
}

function getBin(px, py) {
var u = transform(px, py);
px = u[0];
py = u[1];
var pj = Math.round(py = py / dy),
pi = Math.round(px = px / dx - (pj & 1) / 2),
py1 = py - pj;

if (Math.abs(py1) * 3 > 1) {
var px1 = px - pi,
pi2 = pi + (px < pi ? -1 : 1) / 2,
pj2 = pj + (py < pj ? -1 : 1),
px2 = px - pi2,
py2 = py - pj2;
if (px1 * px1 + py1 * py1 > px2 * px2 + py2 * py2) pi = pi2 + (pj & 1 ? 1 : -1) / 2, pj = pj2;
}
return [pi, pj];
}

function addOne(point, px, py) {
var b = getBin(px, py), pi = b[0], pj = b[1], id = b[0] + "-" + b[1], bin = binsById[id];
if (bin) bin.push(point);
else {
bins.push(bin = binsById[id] = [point]);
var u = untransform((pi + (pj & 1) / 2) * dx, pj * dy);
bin.x = u[0];
bin.y = u[1];
}
}

function addAll(points) {
points = Array.from(points);
var i, point, px, py, n = points.length;

for (i = 0; i < n; ++i) {
if (isNaN(px = +x.call(null, point = points[i], i, points))
|| isNaN(py = +y.call(null, point, i, points))) continue;
addOne(point, px, py);
}
}

var point,
px,
py,
pj = Math.round(py = py / dy),
pi = Math.round(px = px / dx - (pj & 1) / 2),
py1 = py - pj;

if (Math.abs(py1) * 3 > 1) {
var px1 = px - pi,
pi2 = pi + (px < pi ? -1 : 1) / 2,
pj2 = pj + (py < pj ? -1 : 1),
px2 = px - pi2,
py2 = py - pj2;
if (px1 * px1 + py1 * py1 > px2 * px2 + py2 * py2) pi = pi2 + (pj & 1 ? 1 : -1) / 2, pj = pj2;
}
function removeFromBin(point, bin) {
var i = bin.indexOf(point);
if (i > -1) {
bin.splice(i, 1);
}
}

var id = pi + "-" + pj, bin = binsById[id];
if (bin) bin.push(point);
else {
bins.push(bin = binsById[id] = [point]);
bin.x = (pi + (pj & 1) / 2) * dx;
bin.y = pj * dy;
function remove(point) {
var px, py;
if (isNaN(px = +x.call(null, point))
|| isNaN(py = +y.call(null, point))) return;
var b = getBin(px, py), id = b[0] + "-" + b[1], bin = binsById[id];
if (bin) {
removeFromBin(point, bin);
if (bin.length == 0) {
var i = bins.indexOf(bin);
if (i > -1) {
bins.splice(i, 1);
delete binsById[id];
}
}
}

return bins;
}

function hexagon(radius) {
var x0 = 0, y0 = 0;
return angles.map(function(angle) {
var x1 = Math.sin(angle) * radius,
y1 = -Math.cos(angle) * radius,
dx = x1 - x0,
dy = y1 - y0;
x0 = x1, y0 = y1;
return [dx, dy];
return angles.map(function(a) {
a -= angle / 180 * Math.PI;
return [ Math.sin(a) * radius, -Math.cos(a) * radius ];
});
}

function vectors(points) {
for (var i = points.length - 1; i > 0; i--) {
points[i][0] -= points[i-1][0];
points[i][1] -= points[i-1][1];
}
}

hexbin.hexagon = function(radius) {
return "m" + hexagon(radius == null ? r : +radius).join("l") + "z";
hexbin.hexagon = function(radius, translate) {
if (typeof radius == "object") {
var tmp = translate;
translate = radius;
radius = tmp;
}
var points = hexagon(radius == null ? r : +radius);
if (!context) {
vectors(points);
return (translate ? "M" + translate : "") + "m" + points.join("l") + "z";
}
if (translate == null) translate = [0, 0];
context.moveTo(translate[0] + points[0][0], translate[1] + points[0][1]);
for (var i = 1; i < 6; i++)
context.lineTo(translate[0] + points[i][0], translate[1] + points[i][1]);
};

hexbin.centers = function() {
var centers = [],
j = Math.round(y0 / dy),
i = Math.round(x0 / dx);
for (var y = j * dy; y < y1 + r; y += dy, ++j) {
for (var x = i * dx + (j & 1) * dx / 2; x < x1 + dx / 2; x += dx) {
centers.push([x, y]);
var u00 = transform(x0, y0), tx00 = u00[0], ty00 = u00[1],
u10 = transform(x1, y0), tx10 = u10[0], ty10 = u10[1],
u01 = transform(x0, y1), tx01 = u01[0], ty01 = u01[1],
u11 = transform(x1, y1), tx11 = u11[0], ty11 = u11[1],
tx0 = Math.min(tx00, tx01, tx10, tx11),
ty0 = Math.min(ty00, ty01, ty10, ty11),
tx1 = Math.max(tx00, tx01, tx10, tx11),
ty1 = Math.max(ty00, ty01, ty10, ty11),
centers = [],
j = Math.floor(ty0 / dy),
i = Math.floor(tx0 / dx);

for (var y = j * dy; y < ty1 + r; y += dy, ++j) {
for (var x = i * dx + (j & 1) * dx / 2; x < tx1 + dx / 2; x += dx) {
var u = untransform(x, y), ux = u[0], uy = u[1];
if (ux >= x0 - dx && ux <= x1 + dx && uy >= y0 - dy && uy <= y1 + dy)
centers.push([ux, uy]);
}
}
return centers;
};

hexbin.mesh = function() {
var fragment = hexagon(r).slice(0, 4).join("l");
return hexbin.centers().map(function(p) { return "M" + p + "m" + fragment; }).join("");
var points = hexagon(r).slice(0, 4),
centers = hexbin.centers();
if (!context) {
vectors(points);
var fragment = points.join("l");
return centers.map(function(p) { return "M" + p + "m" + fragment; }).join("");
}
for (var i = 0, l = centers.length; i < l; i++) {
var x0 = centers[i][0], y0 = centers[i][1];
context.moveTo(x0 + points[0][0], y0 + points[0][1]);
for (var j = 1; j < 4; j++)
context.lineTo(x0 + points[j][0], y0 + points[j][1]);
}
};

hexbin.angle = function(_) {
return arguments.length ? (angle = _, ca = Math.cos(angle * Math.PI/180), sa = Math.sin(angle * Math.PI/180), hexbin) : angle;
};

hexbin.translate = function(_) {
return arguments.length ? (tx = _[0], ty = _[1], hexbin) : [tx, ty];
};

hexbin.x = function(_) {
Expand All @@ -108,5 +214,46 @@ export default function() {
return arguments.length ? (x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1], hexbin) : [[x0, y0], [x1, y1]];
};

hexbin.context = function(_) {
return arguments.length ? (context = _, hexbin) : context;
}

hexbin.bin = function(point) {
var px, py;
if (isNaN(px = +x.call(null, point))
|| isNaN(py = +y.call(null, point))) return;
var b = getBin(px, py), pi = b[0], pj = b[1], id = b[0] + "-" + b[1], bin = binsById[id];
if (!bin) {
bin = [];
var u = untransform((pi + (pj & 1) / 2) * dx, pj * dy);
bin.x = u[0];
bin.y = u[1];
}
return bin;
}

hexbin.add = function(point) {
var px, py;
if (!isNaN(px = +x.call(null, point, 0, [point]))
&& !isNaN(py = +y.call(null, point, 0, [point])))
addOne(point, px, py);
return hexbin;
}

hexbin.addAll = function(points) {
Array.from(points).forEach(hexbin.add);
return hexbin;
}

hexbin.remove = function(point) {
remove(point);
return hexbin;
}

hexbin.removeAll = function(points) {
Array.from(points).forEach(remove);
return hexbin;
}

return hexbin.radius(1);
}
16 changes: 16 additions & 0 deletions test/compare-images
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash

for i in hexagons; do
test/render-canvas $i > test/output/$i.png \
&& [ "$(gm compare -metric rmse img/$i.png test/output/$i.png 2>&1)" = "Image Difference (RootMeanSquaredError):
Normalized Absolute
============ ==========
Red: 0.0000000000 0.0
Green: 0.0000000000 0.0
Blue: 0.0000000000 0.0
Total: 0.0000000000 0.0" ] \
&& echo -e "\x1B[1;32m✓ $2\x1B[0mtest/output/$i.png" \
&& rm -f -- test/output/$i-difference.png \
|| (gm compare -type TrueColor -highlight-style assign -highlight-color red -file test/output/$i-difference.png test/output/$i.png img/$i.png; \
echo -e "\x1B[1;31m✗ $2\x1B[0mtest/output/$i.png\n test/output/$i-difference.png")
done
Loading