Sketching mirror graph with extra information. In each row:
xxxxxxxxxx
<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>
https://d3js.org/d3.v4.min.js