|
<!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); |
|
|
|
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> |