Skip to content

Instantly share code, notes, and snippets.

@daniel-barrows
Forked from ChrisManess/.block
Last active December 1, 2017 23:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save daniel-barrows/5af62ca13e3997fa8463b60ddbb19dbf to your computer and use it in GitHub Desktop.
Save daniel-barrows/5af62ca13e3997fa8463b60ddbb19dbf to your computer and use it in GitHub Desktop.
Criminal Procedures
license: gpl-3.0

This is an unfinished work, but is someday intended to be a reformatted version of a chart from the Bureau of Justice Statistics. That chart uses weighted lines, but is not based on actual data to determine the weight of those lines. The code for this is copied directly from Chris Maness Alluvial-Growth library, which uses D3.js.

d3.alluvialGrowth = function() {
var alluvialGrowth = {},
nodeWidth = 24,
nodePadding = 8,
size = [1, 1],
nodes = [],
links = [];
alluvialGrowth.nodeWidth = function(_) {
if (!arguments.length) return nodeWidth;
nodeWidth = +_;
return alluvialGrowth;
};
alluvialGrowth.nodePadding = function(_) {
if (!arguments.length) return nodePadding;
nodePadding = +_;
return alluvialGrowth;
};
alluvialGrowth.nodes = function(_) {
if (!arguments.length) return nodes;
nodes = _;
return alluvialGrowth;
};
alluvialGrowth.links = function(_) {
if (!arguments.length) return links;
links = _;
return alluvialGrowth;
};
alluvialGrowth.size = function(_) {
if (!arguments.length) return size;
size = _;
return alluvialGrowth;
};
alluvialGrowth.layout = function(iterations) {
computeNodeLinks();
computeNodeValues();
computeNodeBreadths();
computeNodeDepths(iterations);
computeLinkDepths();
return alluvialGrowth;
};
alluvialGrowth.relayout = function() {
computeLinkDepths();
return alluvialGrowth;
};
alluvialGrowth.link = function() {
var curvature = .5;
function link(d) {
var x0 = d.source.x + d.source.dx,
x1 = d.target.x,
xi = d3.interpolateNumber(x0, x1),
x2 = xi(curvature),
x3 = xi(1 - curvature),
y0 = d.source.y + d.sy,
ytr = d.target.y + d.ety,
ybr = ytr + d.edy,
ybl = y0 + d.dy;
return "M" + x0 + "," + y0 //top left corner
+ "C" + x2 + "," + y0 //top left curve
+ " " + x3 + "," + ytr //top right curve
+ " " + x1 + "," + ytr //Top right corner
+ "L" + x1 + "," + ybr //bottom right corner
+ "C" + x3 + "," + ybr //bottom right curve
+ " " + x2 + "," + ybl //bottom left curve
+ " " + x0 + "," + ybl //bottom left corner
+ "L" + x0 + "," + (y0);
}
link.curvature = function(_) {
if (!arguments.length) return curvature;
curvature = +_;
return link;
};
return link;
};
// Populate the sourceLinks and targetLinks for each node.
// Also, if the source and target are not objects, assume they are indices.
function computeNodeLinks() {
nodes.forEach(function(node) {
node.sourceLinks = [];
node.targetLinks = [];
});
links.forEach(function(link) {
var source = link.source,
target = link.target;
if (typeof source === "number") source = link.source = nodes[link.source];
if (typeof target === "number") target = link.target = nodes[link.target];
source.sourceLinks.push(link);
target.targetLinks.push(link);
});
}
// Compute the value (size) of each node by summing the associated links.
function computeNodeValues() {
nodes.forEach(function(node) {
node.value = Math.max(d3.sum(node.sourceLinks, value),
d3.sum(node.targetLinks, endValue),
d3.sum(node.targetLinks, value));
});
}
// Iteratively assign the breadth (x-position) for each node.
// Nodes are assigned the maximum breadth of incoming neighbors plus one;
// nodes with no incoming links are assigned breadth zero, while
// nodes with no outgoing links are assigned the maximum breadth.
function computeNodeBreadths() {
var remainingNodes = nodes,
nextNodes,
x = 0;
while (remainingNodes.length) {
nextNodes = [];
remainingNodes.forEach(function(node) {
node.x = x;
node.dx = nodeWidth;
node.sourceLinks.forEach(function(link) {
if (nextNodes.indexOf(link.target) < 0) {
nextNodes.push(link.target);
}
});
});
remainingNodes = nextNodes;
++x;
}
//
moveSinksRight(x);
scaleNodeBreadths((size[0] - nodeWidth) / (x - 1));
}
function moveSourcesRight() {
nodes.forEach(function(node) {
if (!node.targetLinks.length) {
node.x = d3.min(node.sourceLinks, function(d) { return d.target.x; }) - 1;
}
});
}
function moveSinksRight(x) {
nodes.forEach(function(node) {
if (!node.sourceLinks.length) {
node.x = x - 1;
}
});
}
function scaleNodeBreadths(kx) {
nodes.forEach(function(node) {
node.x *= kx;
});
}
function computeNodeDepths(iterations) {
var nodesByBreadth = d3.nest()
.key(function(d) { return d.x; })
.sortKeys(d3.ascending)
.entries(nodes)
.map(function(d) { return d.values; });
//
initializeNodeDepth();
resolveCollisions();
for (var alpha = 1; iterations > 0; --iterations) {
relaxRightToLeft(alpha *= .99);
resolveCollisions();
relaxLeftToRight(alpha);
resolveCollisions();
}
function initializeNodeDepth() {
var ky = d3.min(nodesByBreadth, function(nodes) {
return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value);
});
nodesByBreadth.forEach(function(nodes) {
nodes.forEach(function(node, i) {
node.y = i;
node.dy = node.value * ky;
});
});
links.forEach(function(link) {
if(typeof link.endValue === "undefined"){
link.edy = link.value * ky;
} else {
link.edy = link.endValue * ky; //added this in to calculate the ending dy
}
link.dy = link.value * ky;
});
}
function relaxLeftToRight(alpha) {
nodesByBreadth.forEach(function(nodes, breadth) {
nodes.forEach(function(node) {
if (node.targetLinks.length) {
var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value);
node.y += (y - center(node)) * alpha;
}
});
});
function weightedSource(link) {
return center(link.source) * link.value;
}
}
function relaxRightToLeft(alpha) {
nodesByBreadth.slice().reverse().forEach(function(nodes) {
nodes.forEach(function(node) {
if (node.sourceLinks.length) {
var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value);
node.y += (y - center(node)) * alpha;
}
});
});
function weightedTarget(link) {
return center(link.target) * link.value;
}
}
function resolveCollisions() {
nodesByBreadth.forEach(function(nodes) {
var node,
dy,
y0 = 0,
n = nodes.length,
i;
// Push any overlapping nodes down.
nodes.sort(ascendingDepth);
for (i = 0; i < n; ++i) {
node = nodes[i];
dy = y0 - node.y;
if (dy > 0) node.y += dy;
y0 = node.y + node.dy + nodePadding;
}
// If the bottommost node goes outside the bounds, push it back up.
dy = y0 - nodePadding - size[1];
if (dy > 0) {
y0 = node.y -= dy;
// Push any overlapping nodes back up.
for (i = n - 2; i >= 0; --i) {
node = nodes[i];
dy = node.y + node.dy + nodePadding - y0;
if (dy > 0) node.y -= dy;
y0 = node.y;
}
}
});
}
function ascendingDepth(a, b) {
return a.y - b.y;
}
}
function computeLinkDepths() {
nodes.forEach(function(node) {
node.sourceLinks.sort(ascendingTargetDepth);
node.targetLinks.sort(ascendingSourceDepth);
});
nodes.forEach(function(node) {
var sy = 0, ty = 0, ety = 0;
node.sourceLinks.forEach(function(link) {
link.sy = sy;
sy += link.dy;
});
node.targetLinks.forEach(function(link) {
link.ety = ety;
ety += link.edy;
link.ty = ty;
ty += link.dy;
});
});
function ascendingSourceDepth(a, b) {
return a.source.y - b.source.y;
}
function ascendingTargetDepth(a, b) {
return a.target.y - b.target.y;
}
}
function center(node) {
return node.y + node.dy / 2;
}
function value(link) {
return link.value;
}
function endValue(link) {
return link.endValue;
}
return alluvialGrowth;
};
{"nodes":[
{"name":"Crime", "id":0},
{"name":"Reported and Observed Crime", "id":1},
{"name":"Arrest", "id":2},
{"name":"Charges filed", "id":3},
{"name":"Initial Appearance", "id":4},
{"name":"Preliminary hearing", "id":5},
{"name":"Bail or detention hearing", "id":6},
{"name":"Felonies", "id":7},
{"name":"Grand Jury", "id":8},
{"name":"Refusal to indict", "id":9},
{"name":"Information", "id":10},
{"name":"Arraignment", "id":11}
],"links":[
{"source":0,"target":1,"value":40, "endValue": 15},
{"source":1,"target":2,"value":15, "endValue": 12},
{"source":2,"target":3,"value":12, "endValue": 10},
{"source":3,"target":4,"value":10, "endValue": 8},
{"source":4,"target":5,"value":8, "endValue": 6},
{"source":5,"target":6,"value":1, "endValue": 3},
{"source":6,"target":7,"value":1, "endValue": 5},
{"source":7,"target":8,"value":1, "endValue": 4},
{"source":8,"target":9,"value":4, "endValue": 1},
{"source":9,"target":10,"value":1, "endValue": 1},
{"source":10,"target":11,"value":1, "endValue": 3}
]}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>alluvialGrowth Diagram</title>
<script data-require="d3@3.5.3" data-semver="3.5.3" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
<style>
@import url(http://fonts.googleapis.com/css?family=PT+Serif|PT+Serif:b|PT+Serif:i|PT+Sans|PT+Sans:b);
svg {
font: 10px sans-serif;
}
#chart {
height: 500px;
}
.node rect {
cursor: move;
fill-opacity: .9;
shape-rendering: crispEdges;
}
.node text {
pointer-events: none;
text-shadow: 0 1px 0 #fff;
}
.link {
fill: none;
stroke: #000;
fill-opacity: .2;
}
.link:hover {
stroke-opacity: .5 !important;
}
.axis text {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
</style>
</head>
<body>
<p id="chart">
<script src="alluvial-growth.js"></script>
<script>
var margin = {top: 1, right: 1, bottom: 6, left: 1},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var formatNumber = d3.format(",.0f"),
format = function(d) { return formatNumber(d) + " TWh"; },
color = d3.scale.category20();
var svg = d3.select("#chart").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 + ")");
var alluvialGrowth = d3.alluvialGrowth()
.nodeWidth(15)
.nodePadding(30)
.size([width, height]);
var path = alluvialGrowth.link();
d3.json("data.json", function(data) {
alluvialGrowth
.nodes(data.nodes)
.links(data.links)
.layout(32);
var link = svg.append("g").selectAll(".link")
.data(data.links)
.enter().append("path")
.attr("class", "link")
.attr("d", path)
.attr("id", function(d,i){
d.id = i;
return "link-"+i;
})
.style("fill", function(d) { return "steelblue";})
.style("stroke-width", 0)
.sort(function(a, b) { return b.dy - a.dy; });
link.append("title")
.text(function(d) { return d.source.name + " → " + d.target.name + "\n" + format(d.value); });
var node = svg.append("g").selectAll(".node")
.data(data.nodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.call(d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", function() { this.parentNode.appendChild(this); })
.on("drag", dragmove));
node.append("rect")
.attr("height", function(d) { return d.dy; })
.attr("width", alluvialGrowth.nodeWidth())
.style("fill", function(d) { return d.color = color(d.name.replace(/ .*/, "")); })
.style("stroke", function(d) { return d3.rgb(d.color).darker(2); })
.append("title")
.text(function(d) { return d.name + "\n" + format(d.value); });
node.append("text")
.attr("x", -6)
.attr("y", function(d) { return d.dy / 2; })
.attr("dy", ".35em")
.attr("text-anchor", "end")
.attr("transform", null)
.text(function(d) { return d.name; })
.filter(function(d) { return d.x < width / 2; })
.attr("x", 6 + alluvialGrowth.nodeWidth())
.attr("text-anchor", "start");
function dragmove(d) {
d3.select(this).attr("transform", "translate(" + d.x + "," + (d.y = Math.max(0, Math.min(height - d.dy, d3.event.y))) + ")");
alluvialGrowth.relayout();
link.attr("d", path);
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment