Click and drag to move states around. Move slider to modify distance function
Fork of mbostock’s force layout example (http://bl.ocks.org/mbostock/1073373) with a few experimental changes.
Create links between neighbouring states (inferred from topojson mesh) rather than from Voronoi triangles which add some unstable cross-country links like Maine-Washington. Parameterize distance function with a slider (0 = spatial similarity only, 1 = more variation based on type similarity).
<!DOCTYPE html>
<html>
<head>
<title>Force-Directed States of America</title>
<script src=""></script>
<script type="text/javascript" src="//d3js.org/d3.v3.min.js"></script>
<script type="text/javascript" src="//d3js.org/topojson.v1.min.js"></script>
<script src="//d3js.org/queue.v1.min.js"></script>
<script src="d3.slider.js"></script>
<!-- <script type="text/javascript" src="//d3js.org/d3.geom.js"></script>
<script type="text/javascript" src="//d3js.org/d3.layout.js"></script>
-->
<link rel="stylesheet" href="d3.slider.css" />
<style type="text/css">
path {
fill: #ddd;
fill-opacity: .8;
stroke: #fff;
stroke-width: 1.5px;
}
line {
stroke: #999;
}
#slider {
width: 80%;
margin: auto;
}
</style>
</head>
<body>
<div id='slidervalue'></div>
<script type="text/javascript">
var w = 1024,
h = 600;
var projection = d3.geo.albersUsa().translate([w/2,h/2]),
path = d3.geo.path().projection(projection),
force = d3.layout.force().size([w, h]);
var svg = d3.select("body").append("svg:svg")
.attr("width", w)
.attr("height", h);
var slider = d3.slider()
.axis(true)
.min(0)
.max(1)
.step(0.01)
.on('slide', function(e,v) {
force.start();
});
d3.select("body")
.append("div")
.attr("id","slider")
.call(slider);
queue()
.defer(d3.json, "world-50m.json")
.defer(d3.csv, "similarity.csv")
.await(ready);
function linkDistance(d) {
var ds = (1. / d.ss - 1)*23,
tf = Math.exp(slider.value()*(1.-8*d.ts));
return ds * tf; //d.distance;
}
function ready(error, world, similarity) {
var nodes = [],
links = [],
nodesbyname = {};
function continentalus(o) {
return o.properties.iso_a2 == 'US'
&& !( o.properties.postal == 'HI' || o.properties.postal == 'AK');
}
var states = topojson.feature(world, world.objects.states).features
.filter(continentalus);
states.forEach(function(d, i) {
//if (d.id == "02" || d.id == "15" || d.id == "72" || d.id == "78") return; // excl AK,HI,PR
var centroid = path.centroid(d);
centroid.x = centroid[0];
centroid.y = centroid[1];
centroid.feature = d;
if (d.properties.name == 'Kansas') centroid.fixed = true;
nodes.push(centroid);
nodesbyname[d.properties.name] = centroid;
/*
for (var j=0; j<i; j++) {
links.push(edge(centroid,nodes[j]));
}
*/
});
var neighbours = { 'New York_Michigan' : true, 'Michigan_New York' : true };
/*
d3.geom.voronoi()
.links(nodes)
.forEach(function(lnk) {
var a = lnk.source.feature.properties.name,
b = lnk.target.feature.properties.name;
neighbours[a + '_' + b] = true;
neighbours[b + '_' + a] = true;
});
*/
topojson.mesh(world, world.objects.states, function (a, b) {
var aok = continentalus(a),
bok = continentalus(b);
if (aok && bok && a !== b) {
neighbours[a.properties.name + '_' + b.properties.name] = true;
neighbours[b.properties.name + '_' + a.properties.name] = true;
}
return false;
});
similarity.forEach(function(r){
if (!(r.a in nodesbyname && r.b in nodesbyname)) return;
if (!(r.a + '_' + r.b in neighbours)) return;
var e = {
source: nodesbyname[r.a],
target: nodesbyname[r.b],
ss: +r.spatialsimilarity,
ts: +r.typesimilarity
};
links.push(e);
});
// randomize position
/* nodes.forEach(function(d) {
var abbrev = d.feature.properties.postal;
if (abbrev == 'FL' || abbrev == 'WA' || abbrev == 'ME') {
d.fixed = true;
return;
}
d.x = w *(0.5 + 0.5* (Math.random()-0.5));
d.y = h *(0.5 + 0.5* (Math.random()-0.5));
});
*/
force
.size([w,h])
.gravity(0)
.nodes(nodes)
.links(links)
.linkDistance(linkDistance)
.start();
var link = svg.selectAll("line")
.data(links)
.enter().append("svg:line")
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
var node = svg.selectAll("g")
.data(nodes)
.enter().append("svg:g")
// draw each region in a coord system centered at its centroid
.attr("transform", function(d) { return "translate(" + -d[0] + "," + -d[1] + ")"; })
.call(force.drag)
.append("svg:path")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.attr("d", function(d) { return path(d.feature); });
node.append('title')
.text(function(d) {return d.feature.properties.name;});
force.on("tick", function(e) {
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
});
}
function edge(a, b) {
var dx = a[0] - b[0], dy = a[1] - b[1];
return {
source: a,
target: b,
distance: Math.sqrt(dx * dx + dy * dy)
};
}
</script>
</body>
</html>
.d3-slider {
position: relative;
font-family: Verdana,Arial,sans-serif;
font-size: 1.1em;
border: 1px solid #aaaaaa;
z-index: 2;
}
.d3-slider-horizontal {
height: .8em;
}
.d3-slider-vertical {
width: .8em;
height: 100px;
}
.d3-slider-handle {
position: absolute;
width: 1.2em;
height: 1.2em;
border: 1px solid #d3d3d3;
border-radius: 4px;
background: #eee;
background: linear-gradient(to bottom, #eee 0%, #ddd 100%);
z-index: 3;
}
.d3-slider-handle:hover {
border: 1px solid #999999;
}
.d3-slider-horizontal .d3-slider-handle {
top: -.3em;
margin-left: -.6em;
}
.d3-slider-axis {
position: relative;
z-index: 1;
}
.d3-slider-axis-bottom {
top: .8em;
}
.d3-slider-axis-right {
left: .8em;
}
.d3-slider-axis path {
stroke-width: 0;
fill: none;
}
.d3-slider-axis line {
fill: none;
stroke: #aaa;
shape-rendering: crispEdges;
}
.d3-slider-axis text {
font-size: 11px;
}
.d3-slider-vertical .d3-slider-handle {
left: -.25em;
margin-left: 0;
margin-bottom: -.6em;
}
/*
D3.js Slider
Inspired by jQuery UI Slider
Copyright (c) 2013, Bjorn Sandvik - http://blog.thematicmapping.org
BSD license: http://opensource.org/licenses/BSD-3-Clause
*/
d3.slider = function module() {
"use strict";
// Public variables width default settings
var min = 0,
max = 100,
step = 1,
animate = true,
orientation = "horizontal",
axis = false,
margin = 50,
value,
scale;
// Private variables
var axisScale,
dispatch = d3.dispatch("slide"),
formatPercent = d3.format(".2%"),
tickFormat = d3.format(".0"),
sliderLength;
function slider(selection) {
selection.each(function() {
// Create scale if not defined by user
if (!scale) {
scale = d3.scale.linear().domain([min, max]);
}
// Start value
value = value || scale.domain()[0];
// DIV container
var div = d3.select(this).classed("d3-slider d3-slider-" + orientation, true);
var drag = d3.behavior.drag();
// Slider handle
var handle = div.append("a")
.classed("d3-slider-handle", true)
.attr("xlink:href", "#")
.on("click", stopPropagation)
.call(drag);
// Horizontal slider
if (orientation === "horizontal") {
div.on("click", onClickHorizontal);
drag.on("drag", onDragHorizontal);
handle.style("left", formatPercent(scale(value)));
sliderLength = parseInt(div.style("width"), 10);
} else { // Vertical
div.on("click", onClickVertical);
drag.on("drag", onDragVertical);
handle.style("bottom", formatPercent(scale(value)));
sliderLength = parseInt(div.style("height"), 10);
}
if (axis) {
createAxis(div);
}
function createAxis(dom) {
// Create axis if not defined by user
if (typeof axis === "boolean") {
axis = d3.svg.axis()
.ticks(Math.round(sliderLength / 100))
.tickFormat(tickFormat)
.orient((orientation === "horizontal") ? "bottom" : "right");
}
// Copy slider scale to move from percentages to pixels
axisScale = scale.copy().range([0, sliderLength]);
axis.scale(axisScale);
// Create SVG axis container
var svg = dom.append("svg")
.classed("d3-slider-axis d3-slider-axis-" + axis.orient(), true)
.on("click", stopPropagation);
var g = svg.append("g");
// Horizontal axis
if (orientation === "horizontal") {
svg.style("left", -margin);
svg.attr({
width: sliderLength + margin * 2,
height: margin
});
if (axis.orient() === "top") {
svg.style("top", -margin);
g.attr("transform", "translate(" + margin + "," + margin + ")")
} else { // bottom
g.attr("transform", "translate(" + margin + ",0)")
}
} else { // Vertical
svg.style("top", -margin);
svg.attr({
width: margin,
height: sliderLength + margin * 2
});
if (axis.orient() === "left") {
svg.style("left", -margin);
g.attr("transform", "translate(" + margin + "," + margin + ")")
} else { // right
g.attr("transform", "translate(" + 0 + "," + margin + ")")
}
}
g.call(axis);
}
// Move slider handle on click/drag
function moveHandle(pos) {
var newValue = stepValue(scale.invert(pos / sliderLength));
if (value !== newValue) {
var oldPos = formatPercent(scale(stepValue(value))),
newPos = formatPercent(scale(stepValue(newValue))),
position = (orientation === "horizontal") ? "left" : "bottom";
dispatch.slide(d3.event.sourceEvent || d3.event, value = newValue);
if (animate) {
handle.transition()
.styleTween(position, function() { return d3.interpolate(oldPos, newPos); })
.duration((typeof animate === "number") ? animate : 250);
} else {
handle.style(position, newPos);
}
}
}
// Calculate nearest step value
function stepValue(val) {
var valModStep = (val - scale.domain()[0]) % step,
alignValue = val - valModStep;
if (Math.abs(valModStep) * 2 >= step) {
alignValue += (valModStep > 0) ? step : -step;
}
return alignValue;
}
function onClickHorizontal() {
moveHandle(d3.event.offsetX || d3.event.layerX);
}
function onClickVertical() {
moveHandle(sliderLength - d3.event.offsetY || d3.event.layerY);
}
function onDragHorizontal() {
moveHandle(Math.max(0, Math.min(sliderLength, d3.event.x)));
}
function onDragVertical() {
moveHandle(sliderLength - Math.max(0, Math.min(sliderLength, d3.event.y)));
}
function stopPropagation() {
d3.event.stopPropagation();
}
});
}
// Getter/setter functions
slider.min = function(_) {
if (!arguments.length) return min;
min = _;
return slider;
}
slider.max = function(_) {
if (!arguments.length) return max;
max = _;
return slider;
}
slider.step = function(_) {
if (!arguments.length) return step;
step = _;
return slider;
}
slider.animate = function(_) {
if (!arguments.length) return animate;
animate = _;
return slider;
}
slider.orientation = function(_) {
if (!arguments.length) return orientation;
orientation = _;
return slider;
}
slider.axis = function(_) {
if (!arguments.length) return axis;
axis = _;
return slider;
}
slider.margin = function(_) {
if (!arguments.length) return margin;
margin = _;
return slider;
}
slider.value = function(_) {
if (!arguments.length) return value;
value = _;
return slider;
}
slider.scale = function(_) {
if (!arguments.length) return scale;
scale = _;
return slider;
}
d3.rebind(slider, dispatch, "on");
return slider;
}