Skip to content

Instantly share code, notes, and snippets.

@jkschneider
Created February 7, 2013 22:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jkschneider/4734864 to your computer and use it in GitHub Desktop.
Save jkschneider/4734864 to your computer and use it in GitHub Desktop.
D3 Beeswarm Layout with Zooming, Axis

##Introduction##

The beeswarm visualization is used to plot highly compact single-dimensional data by allowing data points to be pushed off the principal axis of the data along the normal to that axis. This visualization can be especially useful for time-series data, maintaining the visual ordering of the data points while allowing for localized regions of highly compact data in the view.

Other implementations use force layout, but the force layout simulation naturally tries to reach its equilibrium by pushing data points along both axes, which can be disruptive to the ordering of the data.

This implementation could be improved by replacing the normally distributed random jittering with an intelligent strategy. For my purpose, this sufficed. The total number of iterations over the collision visitor directly affects the probability of collisions in the end state.

##Zooming## To get zooming to work correctly, I have followed some of the great advice here http://bit.ly/14W6TF7.

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>Timeline Tests</title>
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
<style type="text/css">
.nodeHighlighted {
fill: 'yellow'
}
</style>
</head>
<body>
<div id="tooltip">
<strong class="system">&nbsp;</strong>
<span class="label">&nbsp;</span>
</div>
</body>
<script type="text/javascript">
// d3.behavior.zoom().on("zoom", redraw)
var tooltip = d3.select("#tooltip")
var width = 960
var height = 300
var rectWidth = 20
var rectHeight = 10
var nodeR = 10
var nodeStrokeW = 1.5
var scale = 1
var data = d3.range(400).map(function(d, i) {
return {
x : d,
y : 0, // doesn't matter, will be reset before it matters
r : nodeR,
system : 'csa',
label : 'inquiry'
}
})
var dx = function(d) { return d.x }
var dy = function(d) { return d.y }
var dr = function(d) { return d.r }
var colorX = function(d) { return color(d.x) }
var extent = d3.extent(data, dx)
// the range is set in such a way that portions of nodes are not drawn outside the g
var xScale = d3.scale.linear()
.domain(extent)
.range([nodeStrokeW, width - 2*nodeR - nodeStrokeW])
var color = d3.scale.linear()
.domain(extent)
.range(["steelblue", "brown"])
.interpolate(d3.interpolateHsl)
var norm = d3.random.normal(0, 4.0)
var chart = d3.select("body").append("svg")
.attr("width", 960)
.attr("height", 300)
.attr("pointer-events", "all")
.append("g")
.attr("transform", "translate(10,10)")
.on("mousemove", function() {
var x = d3.event.pageX - 10 // subtract translation
followMe.transition()
.duration(10)
.attr("x1", x).attr("x2", x)
})
.call(d3.behavior.zoom().x(xScale).scaleExtent([1, 10]).on("zoom", zoom))
.append('g');
// append a background rectangle to receive the pointer events
// (otherwise zoom only works when the pointer is over a node)
chart.append('rect')
.attr('width', width)
.attr('height', height)
.attr('fill', 'white');
var axis = d3.svg.axis().scale(xScale)
chart.append("g").attr("class", "xAxis").call(axis)
var followMe = chart.append("line")
.attr("x1", 0).attr("x2", 0)
.attr("y1", 0).attr("y2", 400)
.style("stroke", "red")
function zoom() {
chart.select(".xAxis").call(axis);
if(scale != d3.event.scale)
beeswarm()
scale = d3.event.scale
chart.selectAll("circle.node")
.transition(10)
.attr("cx", function(d) { return xScale(d.x) })
.attr("cy", dy)
}
var nodes = chart.selectAll("circle.node")
.data(data)
beeswarm()
nodes.enter().append("circle")
.attr("class", "node")
.attr("cx", function(d) { return xScale(d.x) })
.attr("cy", dy)
.attr("r", dr)
.attr("fill", colorX)
.style("stroke", "black")
.style("stroke-width", 1.5)
.on("mouseover", function(d) {
tooltip.select(".system").html(d.system)
return tooltip.select(".label").html(d.label)
})
.on("mouseout", function(d) {
tooltip.select(".system").html("&nbsp;")
return tooltip.select(".label").html("&nbsp;")
})
.on("click", function(d, i) {
var inRange = function(dPrime, iPrime) {
return Math.pow(i-iPrime, 2) <= 4
}
d3.selectAll("circle.node").filter(inRange).attr("fill", "yellow")
d3.selectAll("circle.node").filter(function(dPrime, iPrime) { return !inRange(dPrime, iPrime) })
.attr("fill", colorX)
})
function beeswarm() {
// reset vertical position
data.map(function(d) {
d.y = height / 2
})
for(var iter = 0; iter < 10; iter++) {
var q = d3.geom.quadtree(data)
for(var i = 0; i < data.length; i++)
q.visit(collide(data[i]))
}
}
function collide(node) {
var r = node.r + 16,
nx1 = xScale(node.x) - r,
nx2 = xScale(node.x) + r,
ny1 = node.y - r,
ny2 = node.y + r;
return function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = xScale(node.x) - xScale(quad.point.x),
y = node.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = node.r + quad.point.r;
if (l < r)
node.y += norm()
}
return xScale(x1) > nx2
|| xScale(x2) < nx1
|| y1 > ny2
|| y2 < ny1
}
}
</script>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment