block by renecnielsen 8347091cb7507e7f4827cc4ea662a803

Quilted Blocks in D3 v4

Full Screen

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.

forked from yelper‘s block: Quilted Blocks in D3 v4

index.html

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