Slopegraph based on Dan Palmer's Slope Graph and tooltips from D3-tip.
Used for a UN Global Pulse project for International Women's Day & CSW 2014
Slopegraph based on Dan Palmer's Slope Graph and tooltips from D3-tip.
Used for a UN Global Pulse project for International Women's Day & CSW 2014
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"> | |
<title>Slopegraph</title> | |
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script> | |
<script src="http://labratrevenge.com/d3-tip/javascripts/d3.tip.min.js"></script> | |
<link rel="stylesheet" href="style.css" type="text/css"> | |
<link href='http://fonts.googleapis.com/css?family=Open+Sans:300' rel='stylesheet' type='text/css'> | |
<link href="http://fonts.googleapis.com/css?family=Libre+Baskerville:400,700" rel="stylesheet" type="text/css"> | |
</head> | |
<body> | |
<div id="slopegraph"></div> | |
<script type="text/javascript"> | |
//Based on http://thisiscave.co.uk/2013/08/04/d3-slope-graph.html | |
//Settings - width, height, margins | |
if ((window.innerHeight > 1000) & (window.innerWidth > 1600)) | |
{ | |
var WIDTH = 950, | |
HEIGHT = 0.85 * window.innerHeight, | |
MARGINS = [83, 0, 10, 225], //Top, right, bottom, left | |
AXISDISTANCE = 500; //Distance between the two y axes | |
} | |
else if (window.innerWidth > 1600) | |
{ | |
var WIDTH = 950, | |
HEIGHT = 900, | |
MARGINS = [83, 0, 10, 225], //Top, right, bottom, left | |
AXISDISTANCE = 500; //Distance between the two y axes | |
} | |
else if ((window.innerWidth < window.innerHeight) & (window.innerWidth < 1025)) | |
{ | |
var WIDTH = 550, | |
HEIGHT = 800, | |
MARGINS = [83, 0, 10, 150], //Top, right, bottom, left | |
AXISDISTANCE = 250; //Distance between the two y axes | |
} | |
else if (window.innerWidth < 1025) | |
{ | |
var WIDTH = 650, | |
HEIGHT = 800, | |
MARGINS = [83, 0, 10, 180], //Top, right, bottom, left | |
AXISDISTANCE = 300; //Distance between the two y axes | |
} | |
else | |
{ | |
var WIDTH = 770, | |
HEIGHT = 1000, | |
MARGINS = [83, 0, 10, 210], //Top, right, bottom, left | |
AXISDISTANCE = 350; //Distance between the two y axes | |
} | |
//var WIDTH = 850, | |
// HEIGHT = 800, | |
// MARGINS = [83, 0, 10, 225], //Top, right, bottom, left | |
// AXISDISTANCE = 400; //Distance between the two y axes | |
// Real size for the chart | |
var CHARTWIDTH = WIDTH - MARGINS[1] - MARGINS[3], | |
CHARTHEIGHT = HEIGHT - MARGINS[0] - MARGINS[2]; | |
//The svg element | |
var svg = d3.select("body").append("svg") | |
.attr({ | |
"height": HEIGHT, | |
"width": WIDTH | |
}); | |
//Visualisation group/container | |
var vis = svg.append("g") | |
.attr({ | |
"id": "slopegraph", | |
"class": "col-md-8", | |
"transform": "translate(" + MARGINS[3] + "," + MARGINS[0] + ")" | |
}); | |
//Popup | |
var tip = d3.tip() | |
.attr('id', "d3-tip") | |
.attr('class', 'd3-tip') | |
.offset([-10, 0]) | |
.html(function(d) { | |
return "<h4>" + d.topic + "</h4>" + "The World: " + d.left + "%" + "<br/>" + "Women's Day & CSW: " + d.right + "%" + "<br/>" + "Difference: " + d3.round(d.right - d.left,2); | |
}); | |
function fixedHide() { | |
tip.hide(); | |
tip.attr("style", "position: absolute; opacity: 0; top: 0px; left: 0px;"); | |
} | |
//Load the data | |
d3.csv("iwd.csv", function(d) { | |
//Tidy up the data depending on input csv | |
dataSlopeGraph = d.map(function(d) { | |
return { | |
"fill": d.Colour, | |
"left": +d["Global"], | |
"right": +d["IWD"], | |
"topic": d.Topic | |
}; | |
//And sort it descending using the value on the right | |
}).sort(function(a, b) { | |
return a.right - b.right; | |
}); | |
//The data | |
var dataSlopeGraph, | |
minSlopeValue = 0, | |
//maxSlopeValue = 48; | |
maxLeftSlopeValue = d3.max(d, function(d) { return +d["Global"];} ), | |
maxRightSlopeValue = d3.max(d, function(d) { return +d["IWD"];} ); | |
if (maxLeftSlopeValue > maxRightSlopeValue) { var maxSlopeValue = (maxLeftSlopeValue + 1);} | |
else { var maxSlopeValue = (maxRightSlopeValue + 1);} | |
//Find max values of each side | |
//Y scale used for the two axes | |
var yScale = d3.scale.linear() | |
.range([CHARTHEIGHT, 0]) | |
.domain([minSlopeValue, maxSlopeValue]); | |
//Left axis | |
var yAxisLeft = d3.svg.axis() | |
.scale(yScale) | |
.ticks(4) | |
.tickFormat(function(d) { | |
return d3.format(",.0f")(d) + "%"; | |
}) | |
.orient("left"), | |
//Right axis | |
yAxisRight = d3.svg.axis() | |
.scale(yScale) | |
.ticks(0) | |
.orient("left"); | |
//Add the axis and gridlines | |
var tmpYAxis = vis.append("g") | |
.attr("class", "axis y") | |
.call(yAxisLeft); | |
//Gridlines | |
tmpYAxis.selectAll(".tick line") | |
.each(function(d) { | |
//Loop through all the ticks and extend them | |
//across the axis distance | |
var line = d3.select(this); | |
line.attr({ | |
"x1": line.attr("x2") - -35, | |
"x2": AXISDISTANCE | |
}); | |
}); | |
//Move the tick labels in between the axes | |
tmpYAxis.selectAll(".tick text") | |
.attr("dx", "35px"); | |
//Left axis title | |
tmpYAxis.append("text") | |
.text("All Post-2015 Tweets") | |
.style("text-anchor", "middle") | |
.attr({ | |
"class": "title", | |
"dy": "-20px" | |
}); | |
tmpYAxis.append("text") | |
.text("All tweets in the world about Post-2015") | |
.style("text-anchor", "middle") | |
.attr({ | |
"class": "subtitle", | |
"dy": "-5px" | |
}); | |
//Right axis, almost the same just without a gridline loop | |
tmpYAxis = vis.append("g") | |
.attr({ | |
"class": "axis y", | |
"transform": "translate(" + AXISDISTANCE + ",0)" | |
}) | |
.call(yAxisRight); | |
tmpYAxis.append("text") | |
.text("IWD & CSW") | |
.style("text-anchor", "middle") | |
.attr({ | |
"class": "title", | |
"dy": "-20px" | |
}); | |
tmpYAxis.append("text") | |
.text("Tweets also about International Women's Day or CSW") | |
.style("text-anchor", "middle") | |
.attr({ | |
"class": "subtitle", | |
"dy": "-5px" | |
}); | |
//This will return a line pathstring based on some data. The data | |
//that I pass in is formatted like so: | |
// [ [0,y-value], [AXISDISTANCE,y-value] ] | |
var line = d3.svg.line() | |
.interpolate("basis") | |
.x(function(d) { | |
//Use the exact x value | |
return d[0]; | |
}) | |
.y(function(d) { | |
//Return a value based on the y scale created | |
return yScale(d[1]); | |
}); | |
//Plot the actual data | |
//Create a group to store each slope. I also apply a mouseover/leave | |
//listener to it. | |
var topicData = vis.selectAll(".topic-data") | |
.data(dataSlopeGraph).enter() | |
.append("g") | |
.attr("class", "topic-data") | |
.on("mouseover", mouseover) | |
.on("mouseleave", mouseleave); | |
//Left circle | |
topicData.append("circle") | |
.attr({ | |
"cy": function(d) { | |
return yScale(d.left); | |
}, | |
"fill": function(d) { | |
return d.fill; | |
}, | |
"r": 6 | |
}) | |
.style("opacity", "0.5"); | |
//Right circle | |
topicData.append("circle") | |
.attr({ | |
"cx": AXISDISTANCE, | |
"cy": function(d) { | |
return yScale(d.right); | |
}, | |
"fill": function(d) { | |
return d.fill; | |
}, | |
"r": 6 | |
}) | |
.style("opacity", "0.5"); | |
//Text label | |
topicData.append("text") | |
.attr({ | |
"class": "legend-item", | |
"dy": "0.3em", | |
"text-anchor": "end", | |
"x": -15, | |
"y": function(d) { | |
return yScale(d.left); | |
} | |
}) | |
.text(function(d) { | |
return d.topic | |
}) | |
.call(tip); | |
topicData.append("text") | |
.attr({ | |
"class": "legend-item", | |
"dy": "0.3em", | |
"text-anchor": "start", | |
"x": AXISDISTANCE + 15, | |
"y": function(d) { | |
return yScale(d.right); | |
} | |
}) | |
.text(function(d) { | |
return d.topic | |
}) | |
.call(tip); | |
//Slope line | |
topicData.append("path") | |
.attr({ | |
"class": "link", | |
"d": function(d) { | |
return line([ | |
[0, d.left], | |
[AXISDISTANCE, d.right] | |
]); | |
} | |
}) | |
.style("stroke", function(d) { | |
return d.fill; | |
}) | |
.style("opacity", "0.5"); | |
function mouseover(d, i) { | |
var topic = d3.select(topicData[0][i]); | |
topic.selectAll("circle").attr("r", 8); | |
topic.select(".link").style("stroke-width", "5px"); | |
topic.selectAll("text").attr("font-weight", "700"); | |
topic.selectAll("text").style("fill", "#000"); | |
topic.selectAll("circle").on("mouseover",tip.show); | |
topic.selectAll("text").on("mouseover",tip.show); | |
topic.selectAll("circle").style("opacity", "1"); | |
topic.select(".link").style("opacity", "1"); | |
} | |
function mouseleave(d, i) { | |
var topic = d3.select(topicData[0][i]); | |
topic.selectAll("circle").attr("r", 6); | |
topic.select(".link").style("stroke-width", ""); | |
topic.selectAll("text").attr("font-weight", "normal"); | |
topic.selectAll("text").style("fill", "#96999b"); | |
topic.selectAll("circle").on("mouseleave", fixedHide); | |
topic.selectAll("text").on("mouseleave", fixedHide); | |
topic.selectAll("circle").style("opacity", "0.5"); | |
topic.select(".link").style("opacity", "0.5"); | |
} | |
}); | |
</script> | |
</body> | |
</html> |
Topic | IWD | Global | Colour | |
---|---|---|---|---|
Political freedoms | 1.40 | 6.51 | #723390 | |
Action taken on climate change | 0.44 | 4.33 | #fcb749 | |
Equality between men and women | 52.02 | 6.68 | #c84699 | |
An honest and responsive government | 0.92 | 19.60 | #2da9e1 | |
Affordable and nutritious food | 0.55 | 2.02 | #b0d256 | |
Freedom from discrimination | 9.79 | 8.56 | #dec0ca | |
Reliable energy at home | 0.54 | 3.46 | #a01c40 | |
Protecting forests rivers and oceans | 0.58 | 5.58 | #71be45 | |
Protection against crime and violence | 21.35 | 3.76 | #387195 | |
Access to clean water and sanitation | 0.37 | 2.32 | #97d3c9 | |
Phone and internet access | 0.06 | 5.43 | #7cb5d6 | |
Better job opportunities | 2.93 | 12.71 | #e8168b | |
Better healthcare | 0.16 | 2.56 | #ca3a28 | |
A good education | 8.40 | 11.63 | #47c0be | |
Support for people who can't work | 0.24 | 1.01 | #233884 | |
Better transport and roads | 0.24 | 3.84 | #fbe792 |
html, body { | |
font-family: "Open Sans", serif; | |
font-size: 12px; | |
} | |
h1 { | |
text-align: center; | |
font-weight: 700; | |
font-size: 1.2em; | |
} | |
h2 { | |
font-weight: 400; | |
text-align: center; | |
} | |
h3 { | |
font-weight: 400; | |
} | |
.navbar-nav { | |
margin: 0 auto; | |
display: table; | |
table-layout: fixed; | |
float:none; | |
} | |
svg { | |
font-family: "Open Sans", serif; | |
} | |
line { | |
shape-rendering: crispEdges; | |
} | |
.title-main { | |
font-family: 'Open Sans', sans-serif; | |
font-size: 0.9em; | |
fill: #5d6263; | |
} | |
.axis {} | |
.axis .domain { | |
fill: none; | |
stroke: #96999b; | |
stroke-width: 1px; | |
} | |
.axis .tick {} | |
.axis .tick line { | |
stroke: #96999b; | |
stroke-dasharray: 5, 3; | |
stroke-width: 1px; | |
} | |
.axis .tick text { | |
fill: #96999b; | |
font-family: 'Open Sans', sans-serif; | |
font-size: 0.8em; | |
} | |
.axis .title { | |
font-family: "Libre Baskerville", serif; | |
font-size: 2.1em; | |
} | |
.axis .subtitle { | |
fill: #5d6263; | |
font-size: 0.9em; | |
} | |
.topic-data {} | |
.topic-data .link { | |
stroke-width: 3px; | |
} | |
.legend-item { | |
cursor: default; | |
} | |
.legend-item { | |
fill: #96999b; | |
font-size: 0.8em; | |
} | |
.d3-tip { | |
position: absolute; | |
line-height: 1; | |
font-weight: 200; | |
padding: 12px; | |
background: rgba(0,0,0,0.75); | |
color: #fff; | |
border-radius: 2px; | |
z-index: 100; | |
} | |
.d3-tip h4 { | |
font-family: "Libre Baskerville", serif; | |
font-size: 1.3em; | |
font-weight: 400; | |
margin-top: 5px; | |
margin-bottom: 5px; | |
} | |
/* Creates a small triangle extender for the tooltip */ | |
.d3-tip:after { | |
box-sizing: border-box; | |
display: inline; | |
width: 100%; | |
line-height: 0.75; | |
color: rgba(0, 0, 0,0.75); | |
content: "\25BC"; | |
position: absolute; | |
text-align: center; | |
z-index: 100; | |
} | |
/* Style northward tooltips differently */ | |
.d3-tip.n:after { | |
margin: -1px 0 0 0; | |
top: 100%; | |
left: 0; | |
/*height: 700px;*/ | |
z-index: 100; | |
} |