Skip to content

Instantly share code, notes, and snippets.

@yelper
Last active June 9, 2017 20:36
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yelper/aa0860f4d35997a3c94df34764be97b9 to your computer and use it in GitHub Desktop.
Save yelper/aa0860f4d35997a3c94df34764be97b9 to your computer and use it in GitHub Desktop.
Quilted Blocks in D3 v4
license: cc-by-sa-4.0

An example of using D3 v4 for making quilted block designs (also called color weaving; see Albers et al. work on visual aggregation for more information). There's probably a way to abstract the quilted block generation in a d3 module, but hopefully the intuition is pretty clear from the code.

The data is randomly generated from a normal distribution, with the color scale quantized around two standard deviations from the mean. Refresh the page to see different distributions of data.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
canvas, svg {
position: absolute;
top: 0;
left: 0;
}
svg {
z-index: 10;
}
.axis-grid line {
stroke: #fff;
stroke-width: 2;
}
</style>
<body>
<script src="//d3js.org/d3.v4.js"></script>
<script>
var svgWidth = 960, svgHeight = 500;
var margin = {top: 200, right: 40, bottom: 200, left: 40};
var width = svgWidth - margin.left - margin.right;
var height = svgHeight - margin.top - margin.bottom;
var normalNoise = d3.randomNormal();
var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
var data = months.map(function(month) {
return {
month: month,
data: d3.range(20).map(function(i) {
return {
key: i,
value: normalNoise()
};
})
};
});
var x = d3.scaleTime()
.domain([new Date(2016, 0, 1) - 1, new Date(2016, 11, 31)])
.rangeRound([0, width]);
var bandwidth = x(new Date(2016, 1, 1));
// colorbrewer.PiYG[11]
var colorScale = d3.scaleQuantize()
.domain([-2, 2])
.range(["#8e0152", "#c51b7d", "#de77ae", "#f1b6da", "#fde0ef", "#f7f7f7", "#e6f5d0", "#b8e186", "#7fbc41", "#4d9221", "#276419"]);
var svg = d3.select('body').append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
svg.append('g')
.attr('class', 'axis axis-x')
.attr('transform', 'translate(0,' + height + ')')
.call(d3.axisBottom(x)
.ticks(d3.timeMonth)
.tickFormat(d3.timeFormat("%B"))
.tickSize(10))
.selectAll("text")
.attr('dx', x(new Date(2016, 0, 15)))
.attr('text-anchor', 'middle');
svg.append('g')
.attr('class', 'axis axis-grid')
.attr('transform', 'translate(0,' + height + ')')
.call(d3.axisBottom(x)
.ticks(d3.timeMonth)
.tickSize(-height)
.tickFormat(function() { return null; }))
.append('line')
.attr('y2', -100)
.attr('x1', width)
.attr('x2', width);
// add color legend
var legendWidth = 250;
var legendScale = d3.scaleBand()
.domain(colorScale.range())
.rangeRound([0, legendWidth])
.padding(0.15);
var blockSize = legendScale.bandwidth();
var legend = svg.append('g')
.attr('class', 'legend')
.attr('transform', 'translate(' + (width - legendWidth) + ',' + (height + 65) + ')');
legend.append('g')
.attr('class', 'axis axis-legend')
.attr('transform', 'translate(0,' + (blockSize+5) + ')')
.call(d3.axisBottom(d3.scaleOrdinal().domain(["-", "0", "+"]).range([0, legendWidth / 2, legendWidth])));
legend.append('text')
.attr('x', legendWidth / 2)
.attr('y', blockSize + 20)
.attr('dy', "1.2em")
.style('font-size', 14)
.style('text-anchor', 'middle')
.text('profit per day');
legend.selectAll('rect.block')
.data(colorScale.range()).enter()
.append('rect')
.attr('class', 'block')
.attr('x', function(d) { return legendScale(d); })
.attr('height', blockSize)
.attr('width', blockSize)
.style("fill", function(d) { return d; })
.style('stroke', "#000")
.style('shape-rendering', 'crispEdges');
var canvas = d3.select('body').append('canvas')
.attr('width', svgWidth)
.attr('height', svgHeight);
var ctx = canvas.node().getContext('2d');
render();
function render() {
data.forEach(function(month, m) {
// containing box is the following:
var w = bandwidth;
var h = height;
var xstart = x(new Date(2016, m, 1)) + margin.left;
var ystart = margin.top;
var mData = month.data.map(function(d) { return d.value; });
// all pixels to fill
for (var i = 0; i < w * h; i++) {
var di = i % mData.length;
if (di == 0) shuffle(mData);
var curx = i % w + xstart;
var cury = Math.floor(i / w) + ystart;
var curdatum = mData[di];
ctx.fillStyle = colorScale(curdatum);
ctx.fillRect(curx, cury, 1, 1);
}
});
// get rid of hanging excess due to rounding of scales
ctx.clearRect(width + margin.left, margin.top, margin.right, height);
};
// fisher-yates shuffling
function shuffle(arr) {
var i = arr.length;
var tmp, ri;
while (i !== 0) {
ri = Math.floor(Math.random() * i--);
tmp = arr[i];
arr[i] = arr[ri];
arr[ri] = tmp;
}
};
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment