Skip to content

Instantly share code, notes, and snippets.

@marktovey
Last active December 20, 2015 14:49
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 marktovey/6150189 to your computer and use it in GitHub Desktop.
Save marktovey/6150189 to your computer and use it in GitHub Desktop.
Democracy2 Policy Force Diagram (in D3)
{"nodes":[{"Department": "ECONOMY", "PolicyName": "ECONOMY", "maxcost": "", "mincost": ""}, {"Department": "TAX", "PolicyName": "TAX", "maxcost": "", "mincost": ""}, {"Department": "FOREIGNPOLICY", "PolicyName": "FOREIGNPOLICY", "maxcost": "", "mincost": ""}, {"Department": "PUBLICSERVICES", "PolicyName": "PUBLICSERVICES", "maxcost": "", "mincost": ""}, {"Department": "TRANSPORT", "PolicyName": "TRANSPORT", "maxcost": "", "mincost": ""}, {"Department": "WELFARE", "PolicyName": "WELFARE", "maxcost": "", "mincost": ""}, {"Department": "LAWANDORDER", "PolicyName": "LAWANDORDER", "maxcost": "", "mincost": ""},{"Department": "PUBLICSERVICES", "PolicyName": "Adult Education Subsidies", "maxcost": "300", "mincost": "160"}, {"Department": "ECONOMY", "PolicyName": "Agriculture Subsidies", "maxcost": "2800", "mincost": "0"}, {"Department": "TAX", "PolicyName": "Airline Tax", "maxcost": "0", "mincost": "0"}, {"Department": "LAWANDORDER", "PolicyName": "Alcohol Law", "maxcost": "0", "mincost": "0"}, {"Department": "TAX", "PolicyName": "Alcohol Tax", "maxcost": "0", "mincost": "0"}, {"Department": "LAWANDORDER", "PolicyName": "Armed Police", "maxcost": "960", "mincost": "300"}, {"Department": "ECONOMY", "PolicyName": "Ban Sunday Shopping", "maxcost": "0", "mincost": "0"}, {"Department": "TRANSPORT", "PolicyName": "BioFuel Subsidies", "maxcost": "400", "mincost": "5"}, {"Department": "FOREIGNPOLICY", "PolicyName": "Border Controls", "maxcost": "280", "mincost": "10"}, {"Department": "TRANSPORT", "PolicyName": "Bus Lanes", "maxcost": "640", "mincost": "100"}, {"Department": "TRANSPORT", "PolicyName": "Bus Subsidies", "maxcost": "1552", "mincost": "125"}, {"Department": "TAX", "PolicyName": "Carbon Tax", "maxcost": "0", "mincost": "0"}, {"Department": "ECONOMY", "PolicyName": "Car Emissions Limits", "maxcost": "80", "mincost": "0"}, {"Department": "TAX", "PolicyName": "Car Tax", "maxcost": "0", "mincost": "0"}, {"Department": "LAWANDORDER", "PolicyName": "CCTV Cameras", "maxcost": "720", "mincost": "400"}, {"Department": "WELFARE", "PolicyName": "Child Benefit", "maxcost": "4320", "mincost": "400"}, {"Department": "WELFARE", "PolicyName": "Childcare Provision", "maxcost": "1520", "mincost": "100"}, {"Department": "FOREIGNPOLICY", "PolicyName": "Citizenship Tests", "maxcost": "50", "mincost": "10"}, {"Department": "ECONOMY", "PolicyName": "Clean Energy Subsidies", "maxcost": "1560", "mincost": "590"}, {"Department": "TRANSPORT", "PolicyName": "Clean Fuel Subsidy", "maxcost": "320", "mincost": "120"}, {"Department": "LAWANDORDER", "PolicyName": "Community Policing", "maxcost": "320", "mincost": "100"}, {"Department": "ECONOMY", "PolicyName": "Consumer Rights", "maxcost": "8", "mincost": "5"}, {"Department": "TAX", "PolicyName": "Corporation Tax", "maxcost": "0", "mincost": "0"}, {"Department": "PUBLICSERVICES", "PolicyName": "Creationism", "maxcost": "0", "mincost": "0"}, {"Department": "LAWANDORDER", "PolicyName": "Curfews", "maxcost": "200", "mincost": "80"}, {"Department": "LAWANDORDER", "PolicyName": "Death Penalty", "maxcost": "0", "mincost": "0"}, {"Department": "LAWANDORDER", "PolicyName": "Detention Without Trial", "maxcost": "2", "mincost": "2"}, {"Department": "WELFARE", "PolicyName": "Disability benefit", "maxcost": "420", "mincost": "50"}, {"Department": "PUBLICSERVICES", "PolicyName": "Faith School Subsidies", "maxcost": "1160", "mincost": "180"}, {"Department": "FOREIGNPOLICY", "PolicyName": "Foreign Aid", "maxcost": "2490", "mincost": "0"}, {"Department": "TRANSPORT", "PolicyName": "Free Bus Passes", "maxcost": "800", "mincost": "220"}, {"Department": "PUBLICSERVICES", "PolicyName": "Free Eye Tests", "maxcost": "240", "mincost": "200"}, {"Department": "PUBLICSERVICES", "PolicyName": "Free School Meals", "maxcost": "320", "mincost": "210"}, {"Department": "LAWANDORDER", "PolicyName": "Gambling", "maxcost": "0", "mincost": "0"}, {"Department": "LAWANDORDER", "PolicyName": "Gated Communities", "maxcost": "0", "mincost": "0"}, {"Department": "LAWANDORDER", "PolicyName": "Handgun Laws", "maxcost": "4", "mincost": "0"}, {"Department": "TAX", "PolicyName": "Hybrid Cars Initiative", "maxcost": "200", "mincost": "5"}, {"Department": "LAWANDORDER", "PolicyName": "ID Cards", "maxcost": "1200", "mincost": "100"}, {"Department": "FOREIGNPOLICY", "PolicyName": "Import Tariffs", "maxcost": "0", "mincost": "0"}, {"Department": "TAX", "PolicyName": "Income Tax", "maxcost": "0", "mincost": "0"}, {"Department": "TAX", "PolicyName": "Inheritance Tax", "maxcost": "0", "mincost": "0"}, {"Department": "LAWANDORDER", "PolicyName": "Intelligence Services", "maxcost": "1600", "mincost": "900"}, {"Department": "LAWANDORDER", "PolicyName": "Internet Censorship", "maxcost": "400", "mincost": "250"}, {"Department": "TAX", "PolicyName": "Internet Tax", "maxcost": "0", "mincost": "0"}, {"Department": "LAWANDORDER", "PolicyName": "Jury Trial", "maxcost": "200", "mincost": "200"}, {"Department": "ECONOMY", "PolicyName": "Labour Laws", "maxcost": "160", "mincost": "100"}, {"Department": "LAWANDORDER", "PolicyName": "Legal Aid", "maxcost": "80", "mincost": "10"}, {"Department": "LAWANDORDER", "PolicyName": "Legalize Prostitution", "maxcost": "0", "mincost": "0"}, {"Department": "TAX", "PolicyName": "Luxury Goods Tax", "maxcost": "0", "mincost": "0"}, {"Department": "TAX", "PolicyName": "Married Tax Allowance", "maxcost": "5200", "mincost": "100"}, {"Department": "ECONOMY", "PolicyName": "Maternity Leave", "maxcost": "0", "mincost": "0"}, {"Department": "TAX", "PolicyName": "Micro-Generation Grants", "maxcost": "1100", "mincost": "100"}, {"Department": "FOREIGNPOLICY", "PolicyName": "Military Spending", "maxcost": "15600", "mincost": "2300"}, {"Department": "TRANSPORT", "PolicyName": "National Monorail System", "maxcost": "2320", "mincost": "2100"}, {"Department": "TAX", "PolicyName": "Mortgage Tax Relief", "maxcost": "920", "mincost": "200"}, {"Department": "LAWANDORDER", "PolicyName": "Narcotics", "maxcost": "0", "mincost": "0"}, {"Department": "FOREIGNPOLICY", "PolicyName": "National Service", "maxcost": "80", "mincost": "100"}, {"Department": "PUBLICSERVICES", "PolicyName": "Organ Donation", "maxcost": "8", "mincost": "10"}, {"Department": "ECONOMY", "PolicyName": "Organic Farming Subsidy", "maxcost": "800", "mincost": "190"}, {"Department": "TAX", "PolicyName": "Petrol Tax", "maxcost": "0", "mincost": "0"}, {"Department": "LAWANDORDER", "PolicyName": "Phone Tapping", "maxcost": "40", "mincost": "10"}, {"Department": "ECONOMY", "PolicyName": "Plastic Bag Tax", "maxcost": "0", "mincost": "0"}, {"Department": "LAWANDORDER", "PolicyName": "Police Force", "maxcost": "2320", "mincost": "300"}, {"Department": "ECONOMY", "PolicyName": "Pollution Controls", "maxcost": "24", "mincost": "10"}, {"Department": "LAWANDORDER", "PolicyName": "Prisoner Tagging", "maxcost": "6.4", "mincost": "4"}, {"Department": "LAWANDORDER", "PolicyName": "Prisons", "maxcost": "1920", "mincost": "100"}, {"Department": "TAX", "PolicyName": "Property Tax", "maxcost": "0", "mincost": "0"}, {"Department": "PUBLICSERVICES", "PolicyName": "Public Libraries", "maxcost": "800", "mincost": "50"}, {"Department": "LAWANDORDER", "PolicyName": "Racial Profiling", "maxcost": "0", "mincost": "0"}, {"Department": "TRANSPORT", "PolicyName": "Rail Subsidies", "maxcost": "4800", "mincost": "500"}, {"Department": "ECONOMY", "PolicyName": "Recycling", "maxcost": "240", "mincost": "40"}, {"Department": "TRANSPORT", "PolicyName": "Road Building", "maxcost": "4100", "mincost": "100"}, {"Department": "ECONOMY", "PolicyName": "Rural Development Grants", "maxcost": "400", "mincost": "100"}, {"Department": "TAX", "PolicyName": "Sales Tax", "maxcost": "0", "mincost": "0"}, {"Department": "TRANSPORT", "PolicyName": "Satellite Road Pricing", "maxcost": "2000", "mincost": "2500"}, {"Department": "TRANSPORT", "PolicyName": "Subsidised School Buses", "maxcost": "520", "mincost": "250"}, {"Department": "PUBLICSERVICES", "PolicyName": "School Prayers", "maxcost": "0", "mincost": "0"}, {"Department": "PUBLICSERVICES", "PolicyName": "Science Funding", "maxcost": "520", "mincost": "85"}, {"Department": "ECONOMY", "PolicyName": "Small Business Grants", "maxcost": "4700", "mincost": "100"}, {"Department": "LAWANDORDER", "PolicyName": "Speed Cameras", "maxcost": "8", "mincost": "1"}, {"Department": "PUBLICSERVICES", "PolicyName": "State Health Service", "maxcost": "14400", "mincost": "3000"}, {"Department": "WELFARE", "PolicyName": "State Housing", "maxcost": "6400", "mincost": "1000"}, {"Department": "WELFARE", "PolicyName": "State Pensions", "maxcost": "14000", "mincost": "2500"}, {"Department": "PUBLICSERVICES", "PolicyName": "State Schools", "maxcost": "7360", "mincost": "1000"}, {"Department": "PUBLICSERVICES", "PolicyName": "Stem Cell Research", "maxcost": "160", "mincost": "20"}, {"Department": "ECONOMY", "PolicyName": "Tax Shelters", "maxcost": "400", "mincost": "100"}, {"Department": "PUBLICSERVICES", "PolicyName": "Technology Colleges", "maxcost": "670", "mincost": "185"}, {"Department": "ECONOMY", "PolicyName": "Technology Grants", "maxcost": "3000", "mincost": "450"}, {"Department": "TRANSPORT", "PolicyName": "Telecommuting Initiative", "maxcost": "500", "mincost": "110"}, {"Department": "TAX", "PolicyName": "Tobacco Tax", "maxcost": "0", "mincost": "0"}, {"Department": "TRANSPORT", "PolicyName": "Toll Roads", "maxcost": "0", "mincost": "0"}, {"Department": "WELFARE", "PolicyName": "Unemployed Benefit ", "maxcost": "5440", "mincost": "500"}, {"Department": "PUBLICSERVICES", "PolicyName": "University Grants", "maxcost": "2600", "mincost": "200"}, {"Department": "WELFARE", "PolicyName": "Welfare Fraud Dept", "maxcost": "0", "mincost": "0"}, {"Department": "WELFARE", "PolicyName": "Winter Fuel Subsidy", "maxcost": "2000", "mincost": "500"}, {"Department": "PUBLICSERVICES", "PolicyName": "Youth Club Subsidies", "maxcost": "220", "mincost": "12"}, {"Department": "TAX", "PolicyName": "Green Home Standards", "maxcost": "1560", "mincost": "590"}]
}
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.node text {
pointer-events: none;
font: 10px georgia;
font-style: normal;
font-weight:10;
opacity: 0.80;
stroke: black;
}
text {
pointer-events: none;
font: 15px georgia;
font-style: normal;
font-weight:10;
opacity: 0.80;
stroke: black;
}
.link {
stroke: #999;
stroke-opacity: .6;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script>
var width = 950,
height = 500;
var force = d3.layout.force()
.charge(-100)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width+100)
.attr("height", height+100); //added in because the force layout tends to go right to the edge of the bounding box
d3.json("FDJun12.json", function(my_data) {
// using jQuery.map() to reformat the data (as seen in other examples)
// from reading the documentation about nodes and links, it seems that i have to
// return numbers for name, source, target and value/weight
var my_nodes = $.map(my_data.nodes, function(d,i) {return {"name":i, "PolicyName": d.PolicyName, "Department": d.Department, "mincost": d.mincost, "maxcost":d.maxcost };});
/* I create a new array with node names, and include the rest of the node data
so i can call on it & use it when I want
*/
var my_links = $.map(my_data.nodes, function(d,i){
// I create an ordinal scale to convert the department name to a number to use for the target
// value. These numbers correspond to the index value of the department nodes (the order
//they are listed in the json file
deptscale = d3.scale.ordinal()
deptscale.domain(["ECONOMY", "TAX", "FOREIGNPOLICY","PUBLICSERVICES", "TRANSPORT",
"WELFARE", "LAWANDORDER"])
deptscale.range([0,1,2,3,4,5,6])
var returnIndex = deptscale(d.Department)
return {"source":i , "target": returnIndex, "value":1, "origin": d};
});
/* to create the links, i specify a value for source, target, value and origin. source is easy
enough, just the index/name of the node. target is the index of the department the policy belongs to.
the effect of this monkeying around will be to create a central department node
with its policies linked to it. */
//adding node and link data to the force layout
force
.nodes(my_nodes)
.links(my_links)
//modifying link distance to encode policy cost (higher costing policies have longer links
.linkDistance(function(d) {
linkscale = d3.scale.linear()
linkscale.domain([0, 500, 15600])
linkscale.range([5, 70, 300])
distance = linkscale(d.source.maxcost)
return distance})
.start();
// I added in the three points here to stagger the link distances so they would
// be better separated
//I had to hard-code the domain here because I could not make d3.max work at all
// in this example. no idea why...
//creating an svg representation for the links (lines!)
var link = svg.selectAll(".link")
.data(my_links)
.enter().append("line")
.attr("class", "link")
//creating a scale to set radius for the circles so that circle area corresponds to policy
//cost
scale = d3.scale.sqrt()
maxnum = d3.max(my_nodes, function(d) {return parseFloat(d.maxcost);})
console.log("maxnum is")
console.log(maxnum)
scale.domain([0, 15600])
scale.range([5,40])
//making colour scale
colourscale = d3.scale.category10()
colourscale.domain(["ECONOMY", "TAX", "FOREIGNPOLICY","PUBLICSERVICES", "TRANSPORT",
"WELFARE", "LAWANDORDER"])
//creating a svg node class that will eventually include circles and text labels
var node = svg.selectAll(".node")
.data(my_nodes)
.enter().append("g")
.attr("class", "node")
.attr("data-legend", function(d) {return d.Department}) // this is for the legend
/* we're careful to create a g/node class here and not immediately add the circle.
if we added the circle immediately and called this variable node, we wouldn't be able to
add text and have it show up on the display. this is because you can't add text to a
circle. general rule - create a container class and add all things you want to display to
that instead. g/node is our container class. it has all our data on it as well.
*/
node.append("circle")
.attr("r", function (d) {return scale(d.maxcost);})
.attr("class", "node")
.style("fill", function (d) {
return colourscale(d.Department)})
// turning circles gold on mouseover
.on("mouseover", function(){d3.select(this).style("fill", "gold")})
.on("mouseout", function(){d3.select(this).style("fill", function(d)
{return colourscale(d.Department)})
})
//giving circles titles, so they'll show up as tooltips on mouseover (fyi: load time is slow)
node.selectAll("circle")
.append("svg:title")
.text(function(d) { return d.PolicyName +" (" + String(d.maxcost) + ")" })
//creating and hiding text
node.append("text")
.attr("dy", 10)
.attr("z-index", 100)
.text(function(d) {
var PolicyName = d.PolicyName
if (PolicyName == "ECONOMY" || PolicyName == "WELFARE" || PolicyName == "FOREIGNPOLICY" ||
PolicyName == "TRANSPORT" || PolicyName == "PUBLICSERVICES" || PolicyName == "TAX" || PolicyName == "LAWANDORDER")
{return d.PolicyName}
else
{return returnText = PolicyName +" (" + String(d.maxcost) + ")" }
return returnText})
//this text function returns just the department name if it's a department node
// and the policy name and maxcost if it is a policy
.style("visibility",
// here we write a function to make the department nodes visible from the get-go
function(d) { var PolicyName = d.PolicyName
if (PolicyName == "ECONOMY" || PolicyName == "WELFARE" || PolicyName == "FOREIGNPOLICY" ||
PolicyName == "TRANSPORT" || PolicyName == "PUBLICSERVICES" || PolicyName == "TAX" || PolicyName == "LAWANDORDER")
{return "visible"}
else
{return "hidden"}})
// but makes all the policy names invisible
// toggling visibility with click (THANKS NATHAN!)
node.on("click", function() {
var visibility = d3.select(this).select("text").style("visibility")
if (visibility == "visible"){
d3.select(this).select("text").style("visibility", "hidden")}
else
{d3.select(this).select("text").style("visibility", "visible")}
})
//this business below makes the force layout work.
node.call(force.drag);
force.on("tick", function() {
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 + ")"; });
});
//creating a legend
// using a legend plug-in from Ziggy, with some small modifications
// d3.legend.js
// (C) 2012 ziggy.jonsson.nyc@gmail.com
// MIT licence
(function() {
d3.legend = function(g) {
g.each(function() {
var g= d3.select(this),
items = {},
svg = d3.select(g.property("nearestViewportElement")),
legendPadding = g.attr("data-style-padding") || 5,
lb = g.selectAll(".legend-box").data([true]),
li = g.selectAll(".legend-items").data([true])
lb.enter().append("rect").classed("legend-box",true)
li.enter().append("g").classed("legend-items",true)
svg.selectAll("[data-legend]").each(function() {
var self = d3.select(this)
var circle = self.selectAll("circle")
items[self.attr("data-legend")] = {
pos : self.attr("data-legend-pos") || this.getBBox().y,
color : circle.attr("data-legend-color") != undefined ? circle.attr("data-legend-color") : circle.style("fill") != 'none' ? circle.style("fill") : circle.style("stroke")
}
})
items = d3.entries(items).sort(function(a,b) { return a.value.pos-b.value.pos})
ligroups = li.selectAll("g")
.data(items,function(d) { return d.key})
.call(function(d) { d.enter().append("g")})
.call(function(d) { d.exit().remove()})
.attr("node-vis", "hidden")
.on("click", function(d) {
var self = d3.select(this);
self.attr("node-vis", function(){
if(self.attr("node-vis") == "hidden"){return "visible"}
else{return "hidden"}})
d3.selectAll("[data-legend=" + d.key + "]").selectAll("text").style("visibility",self.attr("node-vis"))});
ligroups.append("text")
.attr("y",function(d,i) { return i+"em";})
.attr("x","1em")
.text(function(d) { return d.key;})
.style("font-family", "georgia")
ligroups.append("circle")
.attr("cy",function(d,i) { return i-0.25+"em"})
.attr("cx",0)
.attr("r","0.4em")
.style("fill",function(d) { console.log(d.value.color);return d.value.color})
// Reposition and resize the box
var lbbox = li[0][0].getBBox()
lb.attr("x",(lbbox.x-legendPadding))
.attr("y",(lbbox.y-legendPadding))
.attr("height",(lbbox.height+2*legendPadding))
.attr("width",(lbbox.width+2*legendPadding))
.attr("fill", "none")
})
return g
}
})()
//actually creating the legend
legend = svg.append("g")
.attr("class","legend")
.attr("transform","translate(50,50)")
.style("font-size","15px")
.call(d3.legend)
//adding a title
svg.append("text")
.attr("x", (46))
.attr("y", 20)
.attr("text-anchor", "left")
.style("font-size", "16px")
.text("D2 Policy Browser");
});
//so there you have it!
// ideas for improvements:
/*
1. get d3.max working.
2. policy names toggle visibility for an entire department when you click the circle on
the legend
3. integration into the policy editor. double click takes you to the policy editing page
and changes in policy editor are represented in the visualization (so adding in update/exit
functions)
*/
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment