An update to the horizon bar chart to allow for negative values, using the 'offset' and 'mirror' modes as seen with the area horizon chart (eg https://bl.ocks.org/mbostock/1483226).
Built with blockbuilder.org
forked from tomshanley's block: Horizon bar chart
forked from tomshanley's block: Horizon bar chart v2 (variable bands)
forked from tomshanley's block: Horizon bar chart v3 (mirror v offset)
xxxxxxxxxx
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<style>
body {
font-family: sans-serif;
margin: 0;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
rect,
line {
shape-rendering: crispEdges
}
</style>
</head>
<body>
<h2>Horizon bar chart</h2>
<div id="horizon-controls">
<p>Choose number of bands:
<select id="bands-select">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="10">10</option>
</select>
</p>
<input name="mode" type="radio" value="mirror" id="horizon-mode-mirror" checked><label for="horizon-mode-mirror"> Mirror</label>
<input name="mode" type="radio" value="offset" id="horizon-mode-offset"><label for="horizon-mode-offset"> Offset</label>
</div>
<div id="horizon"></div>
<script>
const maxY = 100;
const minY = -(maxY);
//defaults
var numberOfBands = 4;
var bandWidth = maxY/numberOfBands;
let mode = "mirror"; //or offset
//let mode = "offset";
const height = 80;
const width = 800;
const margin = { "top": 10, "bottom": 10, "left": 50, "right": 10, };
const xScale = d3.scaleLinear()
.range([0, width]);
let colour = d3.scaleSequential(d3.interpolateRdYlGn)
.domain([minY,maxY])
var bandsSelect = d3.select("#bands-select");
bandsSelect.property("value", numberOfBands);
var modeSelect = d3.selectAll("#horizon-controls input[name=mode]");
d3.csv("data.csv", convertTextToNumbers, function(error, data){
if (error) { throw error; };
modeSelect.on("change", function() {
mode = this.value;
drawHorizon(data);
drawLegend();
});
bandsSelect.on("change", function(d){
var selectedBand = d3.select("select").property("value");
numberOfBands = +selectedBand;
bandWidth = maxY/numberOfBands;
drawHorizon(data);
drawLegend();
});
drawHorizon(data);
drawLegend();
});
function convertTextToNumbers(d) {
d.value = +d.value
return d;
};
function drawHorizon(data) {
d3.selectAll("svg").remove();
let yScale = d3.scaleLinear()
.domain([0, bandWidth])
.range([height, 0]);
var nestedBySeries = d3.nest()
.key(function(d){ return d.series })
.entries(data);
nestedBySeries.forEach(function(series){
let seriesData = series.values;
let barWidth = width / seriesData.length - 1;
xScale.domain([0, seriesData.length]);
let svg = d3.select("#horizon").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
let g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
let bars = g.selectAll(".bars")
.data(seriesData)
.enter()
.append("g")
.attr("transform", function (d, i) {
return "translate(" + xScale(i) + ",0)";
});
let backgroundBars = bars.append("rect")
.attr("width", barWidth)
.attr("height", height)
.style("fill", function (d) {
return Math.abs(d.value) < bandWidth
? "white"
: colour(band(d.value, bandWidth));
});
let foregroundBars = bars.append("rect")
.attr("y", function (d) {
if (mode == "offset" && d.value < 0) {
return yScale(bandWidth)
} else {
let thisHeight = barHeight(d.value, bandWidth);
return yScale(thisHeight);
};
})
.attr("width", barWidth)
.attr("height", function (d) {
let thisHeight = barHeight(d.value, bandWidth);
return height - yScale(thisHeight);
})
.style("fill", function (d) {
let thisBand = d.value > 0
? band(d.value, bandWidth) + bandWidth
: band(d.value, bandWidth) - bandWidth
return colour(thisBand);
});
});
};
function drawLegend() {
let legendWidth = 25;
let numberOfLegendItems = numberOfBands * 2;
let legendHeight = legendWidth * numberOfLegendItems;
const legendMargin = {"top": 10, "bottom": 10, "left": 25, "right": 150 };
let legend = d3.select("body").append("svg")
.attr("width", legendWidth + legendMargin.left + legendMargin.right)
.attr("height", legendMargin.top + legendHeight + legendMargin.bottom)
.append("g")
.attr("transform", "translate(" + legendMargin.left + "," + legendMargin.top + ")");
let legendData = [];
let i = 0;
for (i; i < numberOfLegendItems; i++) {
let datum = minY + (i * bandWidth) + bandWidth;
legendData.push(datum)
};
let legendItems = legend.selectAll("g")
.data(legendData)
.enter()
.append("g")
.attr("transform", function(d, j) {
return "translate(0," + (j * legendWidth) + ")"
});
legendItems.append("rect")
.attr("width", legendWidth)
.attr("height", legendWidth)
.style("fill", function(d) {
return d <= 0
? colour(d - bandWidth)
: colour(d);
})
.style("stroke", "white");
legendItems.append("text")
.text(function(d){
return round(d - bandWidth) + " to " + round(d);
})
.attr("x", legendWidth + 5)
.attr("y", legendWidth/2 + 5)
};
function band(n, bandWidth) {
let band = n > 0
? Math.floor(n / bandWidth) * bandWidth
: Math.ceil(n / bandWidth) * bandWidth;
return band;
};
function barHeight(n, bandWidth) {
let absoluteN = Math.abs(n)
return absoluteN - band(absoluteN, bandWidth);
};
function round(n){
return Math.round(n * 10)/10;
};
</script>
</body>
https://d3js.org/d3.v4.min.js
https://d3js.org/d3-scale-chromatic.v1.min.js