Skip to content

Instantly share code, notes, and snippets.

@LemoNode
Last active November 13, 2018 18:20
Show Gist options
  • Save LemoNode/cae7896d9a34d2d13b28a7f78381e9ac to your computer and use it in GitHub Desktop.
Save LemoNode/cae7896d9a34d2d13b28a7f78381e9ac to your computer and use it in GitHub Desktop.
Diverging horizontal bars
license: gpl-3.0
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v5.min.js"></script>
<style>
body {
padding-top: 45px;
margin: auto;
width: 700px;
font: 11px arial;
}
.hide path ,
.hide line {
display: none;
}
.bar--pos {
transition: 0.5s;
transition-property: fill;
fill: steelblue;
}
.bar--neg {
transition: 0.5s;
transition-property: fill;
fill: crimson;
}
.text--pos {
text-anchor:end;
}
.text--neg {
text-anchor:start;
}
</style>
</head>
<body>
<svg id="both" width="325" height="220"></svg>
<svg id="combined" width="325" height="220"></svg>
<select id="options"></select>
<script>
var server = [
{
month: "Q1", new_all: 130, old_all: 100,
new_1: 54, new_2: 32, new_3: 44,
old_1: 34, old_2: 44, old_3: 22
},
{
month: "Q2", new_all: 138, old_all: 98,
new_1: 62, new_2: 62, new_3: 14,
old_1: 4, old_2: 42, old_3: 52
},
{
month: "Q3", new_all: 94, old_all: 74,
new_1: 32, new_2: 52, new_3: 10,
old_1: 10, old_2: 22, old_3: 42
},
{
month: "Q4", new_all: 85, old_all: 65,
new_1: 30, new_2: 46, new_3: 9,
old_1: 9 , old_2: 20, old_3: 36
}
];
load(server);
function load(data) {
var options = d3.select("#options").selectAll("option")
.data(["_1", "_2", "_3", "_all"])
.enter().append("option")
.attr("value", d => d)
.text(d => "Type: " + d)
both(data);
combined(data);
};
function both(data) {
var svg = d3.select("#both"),
margin = {top: 15, right: 10, bottom: 15, left: 10, axis: 15},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var xN = d3.scaleLinear()
.rangeRound([(width / 2) + margin.axis, width - margin.left])
var xO = d3.scaleLinear()
.rangeRound([(width / 2) - margin.axis, margin.right])
var y = d3.scaleBand()
.domain(data.map(d => d.month))
.rangeRound([height - margin.bottom, margin.top])
.padding(0.1);
var xAxisN = svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${(height - margin.bottom)})`);
var xAxisO = xAxisN.clone();
var yAxis = svg.append("g")
.attr("class", "hide")
.style("text-anchor", "middle")
.attr("transform",
`translate(${((margin.left + width + margin.right) / 2)}, 0)`)
.call(d3.axisLeft(y).ticks(null, "s"))
update("_all", 0);
function update(input, speed) {
var max = d3.max(data, d => Math.max(d["new"+input], d["old"+input]))
xN.domain([0, max]).nice();
xO.domain(xN.domain());
xAxisN.transition().duration(speed)
.call(d3.axisBottom(xO).ticks(5));
xAxisO.transition().duration(speed)
.call(d3.axisBottom(xN).ticks(5));
var newInput = svg.selectAll(".newInput")
.data(data, d => d.month);
newInput.exit().remove()
newInput.enter().append("rect")
.attr("class", "newInput bar--pos")
.attr("x", xN(0))
.attr("y", d => y(d.month))
.attr("height", y.bandwidth())
.merge(newInput)
.transition().duration(speed)
.attr("width", d => xN(d["new" + input]) - xN(0));
var oldInput = svg.selectAll(".oldInput")
.data(data, d => d.month);
oldInput.exit().remove()
oldInput.enter().append("rect")
.attr("class", "oldInput bar--neg")
.attr("y", d => y(d.month))
.attr("height", y.bandwidth())
.merge(oldInput)
.transition().duration(speed)
.attr("x", d => xO(d["old" + input]))
.attr("width", d => Math.abs(xO(d["old" + input]) - xO(0)));
}
both.update = update;
}
function combined(data) {
var svg = d3.select("#combined"),
margin = {top: 15, right: 10, bottom: 15, left: 10},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var x = d3.scaleLinear()
.range([margin.left, width - margin.right]);
var y = d3.scaleBand()
.domain(data.map(d => d.month))
.rangeRound([height - margin.bottom, margin.top])
.padding(.1);
var xAxis = d3.axisBottom(x),
yAxis = d3.axisLeft(y).tickSizeOuter(0);
svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${(height - margin.bottom)})`)
svg.append("g")
.attr("class", "y-axis")
.attr("transform", "translate(" + (width/2) + ",0)")
update("_all", 0);
function update(input, speed) {
data.forEach(function(d) {
d.abs = +([d["new" + input] - d["old" + input]]);
return d;
})
x.domain([
d3.min(data, d => -Math.abs(d.abs)),
d3.max(data, d => Math.abs(d.abs))
]).nice();
svg.selectAll(".x-axis").transition().duration(speed)
.call(xAxis);
var bar = svg.selectAll(".bar")
.data(data, d => d.month);
bar.exit().remove()
bar.enter().insert("g", ".y-axis").append("rect")
.attr("class", d => "bar bar--" + (d.abs < 0 ? "neg" : "pos"))
.attr("y", d => y(d.month))
.attr("height", y.bandwidth())
.merge(bar)
.transition().duration(speed)
.attr("class", d => "bar bar--" + (d.abs < 0 ? "neg" : "pos"))
.attr("x", d => x(Math.min(0, d.abs)))
.attr("width", d => Math.abs(x(d.abs) - x(0)));
var text = svg.selectAll(".text")
.data(data, d => d.month);
text.exit().remove();
text.enter().append("text")
.attr("fill", "#fff")
.attr("y", d => y(d.month) + (y.bandwidth() / 2) + 3)
.merge(text)
.transition().duration(speed)
.attr("class", d => "text text--" + (d.abs < 0 ? "neg" : "pos"))
.attr("width", d => Math.abs(x(d.abs) - x(0)))
.attr("x", d => (d.abs <= 0
? x(Math.min(0, d.abs)) + 5
: x(Math.max(0, d.abs)) - 5))
.text(d => {
if (x(d.abs) < ((width / 2) + 15) &&
x(d.abs) > ((width / 2) - 15)) return null;
return d.abs;
})
var ticks = svg.selectAll(".y-axis").transition().duration(speed)
.call(yAxis);
ticks.selectAll("text")
.attr("text-anchor", (_, i) => data[i].abs >= 0 ? "end" : "start")
.attr("x", (_, i) => data[i].abs >= 0 ? -10 : 10);
ticks.selectAll("line")
.attr("x2", (_, i) => data[i].abs >= 0 ? -5 : 5);
}
combined.update = update;
}
var select = d3.select("#options")
.on("change", function() {
both.update(this.value, 750)
combined.update(this.value, 750)
})
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment