Skip to content

Instantly share code, notes, and snippets.

@lorenzopub
Created August 10, 2017 05:31
Show Gist options
  • Save lorenzopub/3ca6560268c172f928c3f1d89f0a686d to your computer and use it in GitHub Desktop.
Save lorenzopub/3ca6560268c172f928c3f1d89f0a686d to your computer and use it in GitHub Desktop.
sketchy horizon bar chart
license: mit

Made sketchy using Elijah Meeks' "cheap sketchy" approach (https://bl.ocks.org/emeeks/8a3a12b0327f12560b1a)

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)

forked from tomshanley's block: Horizon bar chart v3 (mirror v offset)

forked from tomshanley's block: Horizon bar chart v3 (mirror v offset)

forked from tomshanley's block: sketchy horizon bar chart

series value
A 9.5433
A 4.5433
A 4.5433
A 6.0817
A 8.7743
A 10.6971
A 22.2356
A 34.5433
A 37.2356
A 37.6202
A 37.2356
A 35.6971
A 34.5433
A 38.3894
A 43.3894
A 47.6202
A 52.2356
A 55.3125
A 59.1587
A 59.9279
A 58.3894
A 56.851
A 56.0817
A 59.5433
A 61.851
A 64.9279
A 67.6202
A 69.9279
A 69.9279
A 68.004
A 67.2356
A 65.3125
A 62.2356
A 60.3125
A 58.3894
A 57.2356
A 47.6202
A 39.9279
A 1.0817
A 2.6202
A -3.0048
A -6.6202
A -16.4663
A -19.0817
A -39.9279
A -57.2356
A -65.3125
A -82.6202
A -91.0817
A -88.774
A -77.6202
A -66.0817
A -64.5433
A -54.5433
A -45.3125
A -46.4663
A -49.9279
A -40.6971
A -37.6202
A -36.0817
A -35.3125
A -39.5433
A -38.774
A -27.6202
A -20.3125
A -16.851
A 7.6202
A 8.774
A 13.774
A 18.774
A 28.0048
A 36.4663
A 40.3125
B -9.5433
B -4.5433
B -4.5433
B -6.0817
B -8.7743
B -10.6971
B -22.2356
B -34.5433
B -37.2356
B -37.6202
B -37.2356
B -35.6971
B -34.5433
B -38.3894
B -43.3894
B -47.6202
B -52.2356
B -55.3125
B -59.1587
B -59.9279
B -58.3894
B -56.851
B -56.0817
B -59.5433
B -61.851
B -64.9279
B -67.6202
B -69.9279
B -69.9279
B -68.004
B -67.2356
B -65.3125
B -62.2356
B -60.3125
B -58.3894
B -57.2356
B -47.6202
B -39.9279
B -1.0817
B -2.6202
B 3.0048
B 6.6202
B 16.4663
B 19.0817
B 39.9279
B 57.2356
B 65.3125
B 82.6202
B 91.0817
B 88.774
B 77.6202
B 66.0817
B 64.5433
B 54.5433
B 45.3125
B 46.4663
B 49.9279
B 40.6971
B 37.6202
B 36.0817
B 35.3125
B 39.5433
B 38.774
B 27.6202
B 20.3125
B 16.851
B -7.6202
B -8.774
B -13.774
B -18.774
B -28.0048
B -36.4663
B -40.3125
<!DOCTYPE html>
<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
}
path {
fill: none
}
</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
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)
.attr("x", 0)
.attr("y", 0)
.style("fill", function (d) {
return Math.abs(d.value) < bandWidth
? "white"
: colour(band(d.value, bandWidth));
});
let foregroundBars = bars.append("rect")
.attr("x", 0)
.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);
});
});
//MAKE SKETCHY!
d3.selectAll("rect").each(function (d,i) {
let thisRect = d3.select(this);
let parent = d3.select(this.parentNode)
let fill = thisRect.style("fill");
//Create a path based on the rect's coordinates and dimensions
let x = thisRect.attr("x")
let y = thisRect.attr("y");
let width = thisRect.attr("width");
let height = thisRect.attr("height");
let pathString = "m" + "0" + " " + y
+ " l" + width + " " + "0"
+ " l" + "0" + " " + height
+ " l" + (-width) + " " + "0"
+ " Z";
let path = parent.append("path")
.attr("d", pathString)
let pathNode = path.node();
//pass the path's node to the cheapSketchy function
var fillCode = cheapSketchy(pathNode);
//remove the original rect
d3.select(this).remove();
//draw the sketchy path
parent.append("path")
.style("stroke-width", "1px")
.style("stroke", fill)
.style("fill", "none")
.attr("class", "sketchy-fill")
.attr("d", fillCode);
})
};
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;
};
//from https://bl.ocks.org/emeeks/8a3a12b0327f12560b1a
function cheapSketchy(path) {
var length = path.getTotalLength();
var drawCode = "";
var i = 0;
var step = 2;
while (i < length / 2) {
var start = path.getPointAtLength(i);
var end = path.getPointAtLength(length - i);
drawCode += " M" + (start.x + (Math.random() * step - step/2)) + " " + (start.y + (Math.random() * step - step/2)) + "L" + (end.x + (Math.random() * step - step/2)) + " " + (end.y + (Math.random() * step - step/2));
i += step + (Math.random() * step);
}
return drawCode;
}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment