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