|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
|
|
</style> |
|
<body> |
|
<svg> |
|
</svg> |
|
|
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<script> |
|
|
|
//begin: data conf. |
|
var data = [], |
|
min = Infinity, |
|
max = -Infinity, |
|
totalCount = 0; |
|
//end: data conf. |
|
|
|
//begin: layout conf. |
|
var svgWidth = 960, |
|
svgHeight, |
|
|
|
deltaWidth = 50, |
|
conceptNameWidth = 360, |
|
countWidth = 60, |
|
chartWidth = svgWidth - 3*deltaWidth - conceptNameWidth - 2*countWidth, |
|
deltaTotalX = 0, |
|
conceptNameX = deltaTotalX+deltaWidth, |
|
deltaNegativeX = conceptNameX+conceptNameWidth, |
|
chartX = deltaNegativeX+deltaWidth+40, |
|
deltaPositiveX = svgWidth-deltaWidth, |
|
|
|
bandHeight = 20, |
|
barPaddingTop = 2, |
|
barReferenceHeight = 4, |
|
barHeight = bandHeight-2*barPaddingTop-barReferenceHeight, |
|
textY = 14; |
|
//end: layout conf. |
|
|
|
//begin: hack stuff |
|
var currentStage, |
|
currentStageIndex; |
|
//end: hack stuff |
|
|
|
//begin: scales |
|
var y = d3.scaleBand(); |
|
var x = d3.scaleLinear(); |
|
//end: scales |
|
|
|
//begin: reusable d3-selections |
|
var svg = d3.select("svg"); |
|
//end: reusable d3-selections |
|
|
|
d3.csv("apv.csv", parser, function(error, parsedData) { |
|
svgHeight = bandHeight*data.length; |
|
|
|
svg.attr("width", svgWidth) |
|
.attr("height", svgHeight); |
|
|
|
x.range([0, chartWidth]) |
|
.domain([min, max]); |
|
y.range([0, svgHeight]) |
|
.domain(data.map(function(d){ return d.id; })); |
|
|
|
var bands = svg.selectAll(".band") |
|
.data(data) |
|
.enter() |
|
.append("g") |
|
.classed("band", true) |
|
.attr("transform", function(d){ return "translate("+[0, y(d.id)]+")"; }); |
|
|
|
bands.append("rect") |
|
.attr("width", svgWidth) |
|
.attr("height", bandHeight) |
|
.style("fill", function(d){ return (d.stageIndex%2===0)? "#F0F1F3" : "none"; }); |
|
|
|
bands.append("text") |
|
.classed("delta total", true) |
|
.attr("transform", "translate("+[conceptNameX-15,textY]+")") |
|
.attr("text-anchor", "end") |
|
.text(function(d){ return deltaFormater(d.totalDelta, true); }) |
|
.each(textStyler); |
|
|
|
bands.append("text") |
|
.classed("concept-name", true) |
|
.attr("transform", "translate("+[deltaNegativeX-15,textY]+")") |
|
.attr("text-anchor", "end") |
|
.text(function(d){ return d.concept; }) |
|
.each(textStyler); |
|
|
|
bands.filter(function(d,i){ return (i===0 || d.stage!==data[i-1].stage); }).append("text") |
|
.classed("concept-stage", true) |
|
.attr("transform", "translate("+[conceptNameX,textY]+")") |
|
.attr("text-anchor", "start") |
|
.text(function(d,i){ return "["+d.stage+"]"; }) |
|
.each(textStyler); |
|
|
|
bands.append("text") |
|
.classed("delta negative", true) |
|
.attr("transform", "translate("+[deltaNegativeX+deltaWidth-15,textY]+")") |
|
.attr("text-anchor", "end") |
|
.text(function(d){ return deltaFormater(d.negativeDelta); }) |
|
.each(textStyler); |
|
|
|
var rects = bands.append("g") |
|
.attr("transform", "translate("+[chartX,0]+")") |
|
|
|
rects.append("text") |
|
.attr("transform", function(d){ return "translate("+[x(d.negative)-5,textY+barPaddingTop]+")"; }) |
|
.text(function(d){ return labelFormater(Math.abs(d.negative), Math.abs(d.negativePercentage)); }) |
|
.attr("text-anchor", "end") |
|
.each(textStyler); |
|
|
|
rects.append("rect") |
|
.classed("bar negative", true) |
|
.attr("x", function(d){ return x(d.negative); }) |
|
.attr("y", barPaddingTop+barReferenceHeight) |
|
.attr("width", function(d){ return Math.abs(x(d.negative)-x(0)); }) |
|
.attr("height", barHeight) |
|
.style("fill", "#CC7174"); |
|
|
|
rects.append("rect") |
|
.classed("bar positive", true) |
|
.attr("x", function(d){ return x(0); }) |
|
.attr("y", barPaddingTop+barReferenceHeight) |
|
.attr("width", function(d){ return x(d.positive)-x(0); }) |
|
.attr("height", barHeight) |
|
.style("fill", "#98D9A2"); |
|
|
|
rects.append("rect") |
|
.classed("bar negative reference", true) |
|
.attr("x", function(d){ return x(d.negativeFrance); }) |
|
.attr("y", barPaddingTop) |
|
.attr("width", function(d){ return Math.abs(x(d.negativeFrance)-x(0)); }) |
|
.attr("height", barReferenceHeight) |
|
.style("fill", "grey"); |
|
|
|
rects.append("rect") |
|
.classed("bar positive reference", true) |
|
.attr("x", function(d){ return x(0); }) |
|
.attr("y", barPaddingTop) |
|
.attr("width", function(d){ return x(d.positiveFrance)-x(0); }) |
|
.attr("height", barReferenceHeight) |
|
.style("fill", "grey"); |
|
|
|
rects.append("text") |
|
.attr("transform", function(d){ return "translate("+[x(d.positive)+5,textY+barPaddingTop]+")"; }) |
|
.text(function(d){ return labelFormater(d.positive, d.positivePercentage); }) |
|
.attr("text-anchor", "start") |
|
.each(textStyler); |
|
|
|
bands.append("text") |
|
.classed("delta positive", true) |
|
.attr("transform", "translate("+[deltaPositiveX,textY]+")") |
|
.text(function(d){ return deltaFormater(d.positiveDelta); }) |
|
.each(textStyler); |
|
|
|
//begin: axes |
|
svg.append("path") |
|
.classed("y-axis separator", true) |
|
.attr("d", "M"+[conceptNameX-5, 0]+"v"+svgHeight) |
|
.style("stroke", "lightgrey") |
|
.style("shape-rendering", "crispEdges"); |
|
|
|
svg.append("path") |
|
.classed("y-axis separator", true) |
|
.attr("d", "M"+[deltaNegativeX, 0]+"v"+svgHeight) |
|
.style("stroke", "lightgrey") |
|
.style("shape-rendering", "crispEdges"); |
|
|
|
svg.append("path") |
|
.classed("y-axis", true) |
|
.attr("d", "M"+[chartX+x(0), 0]+"v"+svgHeight) |
|
.style("stroke", "grey") |
|
.style("shape-rendering", "crispEdges"); |
|
//end: axes |
|
}); |
|
|
|
|
|
/*****************/ |
|
/* block utility */ |
|
/*****************/ |
|
|
|
function parser(d, i) { |
|
if (d.concept === 'Total') { |
|
//total row must be the first row |
|
totalCount = d.total; |
|
currentStageIndex = 0; |
|
} else { |
|
//subsequent rows requires totalCount |
|
d.id = d.concept; |
|
|
|
d.positive = +d.positive; |
|
d.negative = +d.negative; |
|
d.positiveFrance = +d.positiveFrance |
|
d.negativeFrance = +d.negativeFrance; |
|
|
|
d.positivePercentage = d.positive*100/totalCount; |
|
d.negativePercentage = d.negative*100/totalCount; |
|
d.positivePercentageFrance = d.positiveFrance*100/totalCount; |
|
d.negativePercentageFrance = d.negativeFrance*100/totalCount; |
|
|
|
d.positiveDelta = d.positivePercentage-d.positivePercentageFrance; |
|
d.negativeDelta = d.negativePercentage-d.negativePercentageFrance; |
|
d.totalDelta = d.positiveDelta - d.negativeDelta; |
|
|
|
if (d.stage !== currentStage) { |
|
currentStageIndex += 1; |
|
} |
|
d.stageIndex = currentStageIndex; |
|
|
|
min = Math.min(min, d.negative, d.negativeFrance); |
|
max = Math.max(max, d.positive, d.positiveFrance); |
|
data.push(d); |
|
} |
|
|
|
currentStage = d.stage; |
|
return d; |
|
} |
|
|
|
function deltaFormater(d, forceZero) { |
|
var val = +d.toFixed(1); |
|
if (val===0) { |
|
return forceZero? "0" : ""; |
|
} else if (val<0) { |
|
return val; |
|
} else { |
|
return "+"+val; |
|
} |
|
} |
|
|
|
function labelFormater(count, percentage) { |
|
if (count===0) { |
|
return ""; |
|
} else { |
|
return count + " | " + percentage.toFixed(1) + "%"; |
|
} |
|
} |
|
|
|
function textStyler() { |
|
d3.select(this) |
|
.style("font-size", 12) |
|
.style("font-family", ["Lucida Grande", "Lucida Sans Unicode", "Arial", "Helvetica", "sans-serif"]) |
|
} |
|
|
|
|
|
</script> |