Skip to content

Instantly share code, notes, and snippets.

@veltman
Last active May 1, 2021 23:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save veltman/bbbe7947d1def3303f20 to your computer and use it in GitHub Desktop.
Save veltman/bbbe7947d1def3303f20 to your computer and use it in GitHub Desktop.
Configurable hexbins

Experimenting with an in-browser hexbinner.

To dos: add k-means classification option, allow drag-and-drop data uploads, projection selection, more color scales

See also: Resizing hexbin test

<!DOCTYPE html>
<meta charset="utf-8">
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet" type="text/css">
<style>
body,button {
font: 12px "Open Sans", sans-serif;
}
* {
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
}
a {
text-decoration: none;
}
h2 {
margin: 0 0 0.25em 0;
font-size: 1.5em;
}
h2:first-of-type {
margin-top: 0;
}
.controls {
position: absolute;
top: 1em;
left: 1em;
padding: 1em;
border: 1px solid #444;
}
.controls > div {
margin-bottom: 1em;
}
.controls > div:last-of-type {
margin-bottom: 0;
}
canvas {
position: absolute;
left: 0;
top: 0;
}
.handle {
fill: #fff;
stroke: #000;
stroke-opacity: .5;
stroke-width: 1.25px;
cursor: crosshair;
}
.axis {
pointer-events: none;
}
.axis .domain {
fill: none;
stroke: #000;
stroke-opacity: .3;
stroke-width: 10px;
stroke-linecap: round;
}
.axis .halo {
fill: none;
stroke: #ddd;
stroke-width: 8px;
stroke-linecap: round;
}
.button {
background-color: #fff;
border: 1px solid #ccc;
color: #333;
padding: 0.5em 1em;
line-height: 140%;
vertical-align: middle;
cursor: pointer;
text-align: center;
display: inline-block;
}
.button:hover,
.button:focus,
.button:active,
.button.active {
background-color: #ebebeb;
border-color: #adadad;
}
.button:active,
.button.active {
color: #000;
outline: 0;
-webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
}
.scale .button {
width: 50%;
}
.num-classes .button {
width: 20%;
}
.colors .button {
width: 25%;
}
.colors .button > div {
height: 1em;
width: 100%;
}
.colors .button > div div {
width: 20%;
display: inline-block;
height: 100%;
}
</style>
<body>
<canvas></canvas>
<div class="controls">
<div class="size">
<h2>Hexagon Size</h2>
</div>
<div class="scale">
<h2>Classification</h2>
<div>
<button class="button active">Quantiles</button><button class="button">Equal Intervals</button>
</div>
</div>
<div class="num-classes">
<h2>Number of Classes</h2>
<div>
<button class="button">3</button><button class="button">4</button><button class="button active">5</button><button class="button">6</button><button class="button">7</button>
</div>
</div>
<div class="colors">
<h2>Colors</h2>
<div>
<div class="button active"></div><div class="button"></div><div class="button"></div><div class="button"></div>
</div>
</div>
<div class="save">
<a href="#" class="button" download="hexmap.png">Save Image</a>
</div>
</div>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.10/d3.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/queue-async/1.0.7/queue.min.js"></script>
<script src="hexbin.js"></script>
<script src="colorbrewer.min.js"></script>
<script>
// More NYC-appropriate aspect ratio for bl.ocks.org
d3.select(self.frameElement).style("height", "960px");
var canvas = d3.select("canvas"),
context = d3.select("canvas").node().getContext("2d");
// NY state plane
var projection = d3.geo.conicConformal()
.parallels([40 + 2 / 3, 41 + 1 / 30])
.rotate([74, 40 + 1 / 6]);
var path = d3.geo.path()
.projection(projection);
var color = d3.scale.quantile()
.range(colorbrewer.PuRd[5]);
// Scale for hexagon radius
var radius = d3.scale.linear()
.range([2,30]);
var points,
bg,
clip;
var hexbinner = d3.hexbin().radius(5);
// Get data
queue()
.defer(d3.json,"nyc.geojson")
.defer(d3.csv,"service-requests-311.csv") // 41k random 311 service request locations
.await(ready);
function ready(err,nyc,data) {
// Convert to lng/lat pairs
points = data.map(function(p){
return [+p.lng,+p.lat];
});
// Save bg geography lazily
bg = nyc;
// Draw/add listeners for menus
addHexSize();
addScales();
addNumClasses();
addColors();
d3.select(".save .button").on("click",saveCanvas);
// Keep canvas responsive
window.onresize = resize;
// Set size once
resize();
}
// Redraw everything
function rehex() {
// Wipe map
context.clearRect(0,0,window.innerWidth,window.innerHeight);
// Rebin
var bins = hexbinner(points.map(projection));
// Sorted list of bin counts
var domain = bins.map(function(b){
return b.length;
}).sort(function(a,b){
return a-b;
});
// Update scale domain
color.domain(domain);
context.fillStyle = "#fff";
context.strokeStyle = "#999";
context.fill(clip);
// Clip to background layer
context.globalCompositeOperation = "source-atop";
var hex = new Path2D(hexbinner.hexagon());
bins.forEach(function(bin){
// Draw hexagon
context.translate(bin.x,bin.y);
context.fillStyle = color(bin.length);
context.fill(hex);
// Reset transform
context.setTransform(1, 0, 0, 1, 0, 0);
});
// Doing this on every redraw to avoid weird edges
context.stroke(clip);
updateLegend();
}
function updateLegend() {
var domain = color.domain(),
range = color.range(),
min = domain[0],
max = domain[domain.length - 1],
breaks;
if (color.quantiles) {
breaks = color.quantiles();
breaks.unshift(min);
} else {
breaks = d3.range(range.length).map(function(i) {
return min + (i * (max - min)/range.length);
});
}
// Start at next int for counts
breaks = breaks.map(Math.ceil);
context.globalCompositeOperation = "source-over";
context.textAlign = "left";
context.textBaseline = "top";
context.strokeStyle = "#444";
// Put legend below the Rockaways
var translate = projection([-73.95206451416014,40.53298071625918]);
// Don't stretch legend too far
var width = projection([-73.73542785644531,40.594663726004995])[0] - translate[0];
context.translate(translate[0],translate[1]);
// Clear legend labels lazily
context.clearRect(0, 20, 1000, 1000);
breaks.forEach(function(b,i){
var text = b;
context.fillStyle = range[i];
context.fillRect(0, 0, width / range.length, 20);
if (i === breaks.length - 1) {
text += "+";
}
context.strokeText(text, 0, 20);
context.translate(width / range.length,0);
});
// Leave it at the identity transform
context.setTransform(1, 0, 0, 1, 0, 0);
}
// Get new window size, update all dimensions + projection
function resize() {
var width = window.innerWidth,
height = window.innerHeight;
hexbinner.size([width, height]);
canvas.attr("width",width)
.attr("height",height);
projection.scale(1)
.translate([0,0]);
var b = path.bounds(bg),
s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height),
t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];
projection
.scale(s)
.translate(t);
// Only need to update this onresize
clip = new Path2D(path(bg));
rehex();
}
// Draw slider for hexagon size
function addHexSize(){
var margin = { left: 10, right: 10, top: 8, bottom: 22},
width = 250 - margin.left - margin.right,
height = 36 - margin.top - margin.bottom,
radius = hexbinner.radius();
var x = d3.scale.linear()
.domain([2, 50])
.range([0, width])
.clamp(true);
var brush = d3.svg.brush()
.x(x)
.extent([radius,radius])
.on("brush", brushed);
var axis = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickValues(x.domain())
.tickFormat(function(d,i){
return i ? "Larger" : "Smaller";
})
.tickSize(0)
.tickPadding(12);
var svg = d3.select(".size").append("svg")
.attr("width",width + margin.left + margin.right)
.attr("height",height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height / 2 + ")")
.call(axis)
.select(".domain")
.select(function() { return this.parentNode.appendChild(this.cloneNode(true)); })
.attr("class", "halo");
svg.selectAll("text")
.attr("dx",function(d,i){
return i ? "4px" : "-4px";
})
.style("text-anchor",function(d,i){
return i ? "end" : "start";
});
var slider = svg.append("g")
.attr("class", "slider")
.call(brush);
slider.selectAll(".extent,.resize")
.remove();
slider.select(".background")
.attr("height", height);
var handle = slider.append("circle")
.attr("class","handle")
.attr("r",8)
.attr("cx",x(radius))
.attr("cy",height/2);
function brushed(){
value = x.invert(d3.mouse(this)[0]);
brush.extent([value, value]);
handle.attr("cx", x(value));
hexbinner.radius(value);
rehex();
}
}
// Menu for scale type
function addScales() {
var buttons = d3.selectAll(".scale button")
.data([d3.scale.quantile,d3.scale.quantize]);
buttons.on("click",function(scale){
buttons.classed("active",function(f){
return scale === f;
});
var range = color.range();
color = scale().range(range);
rehex();
});
}
// Menu for number of colors
function addNumClasses() {
var buttons = d3.selectAll(".num-classes button")
.data(d3.range(3,8));
buttons.on("click",function(numClasses){
buttons.classed("active",function(f){
return numClasses === f;
});
var colorScheme = d3.select(".colors .active").datum();
color.range(colorbrewer[colorScheme][numClasses]);
rehex();
});
}
// Menu swatches for color schemes
function addColors() {
var buttons = d3.selectAll(".colors .button")
.data(["PuRd","Blues","OrRd","PiYG"]);
// Draw some swatches
buttons.append("div")
.attr("title",Object)
.selectAll("div")
.data(function(d){
return colorbrewer[d][5];
})
.enter()
.append("div")
.style("background-color",Object);
buttons.on("click",function(colorScheme){
buttons.classed("active",function(f){
return colorScheme === f;
});
var numClasses = color.range().length;
color.range(colorbrewer[colorScheme][numClasses]);
rehex();
});
}
// For downloadable image
function saveCanvas() {
var url = canvas.node().toDataURL();
d3.select(this).attr("href",url);
}
</script>
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View raw

(Sorry about that, but we can’t show files that are this big right now.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment