block by curran a4a39a3d0e89126964aeedcb60d33124

Composable Visualization Test

Full Screen

This is an experiment with the notion of composable visualization components that follow the Towards Reusable Charts pattern (which I think I’m finally grokking after all these years).

The idea is that visualization components can be composed in kind of a recursive way. This would make it straightforward to create, say, stacked bar charts by simply composing components in an almost algebraic expression like barChart * verticalStack * rect. Making that into small multiples would simply require the addition of a facet component to the expression: facetVertical * barChart * verticalStack * rect.

concept

This kind of thing is present in the Grammar of Graphics, the ggplot2 paper, and Vega. It would be amazing to discover straightforward JavaScript/D3 patterns that enable this kind of thing.

Built with blockbuilder.org

diaper cake

index.html

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <title>Composable Visualization Test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
  <style>
    body {
      margin: 0;
    }
  </style>
</head>

<body>
  <script>
    var width = 960;
    var height = 500;
    var svg = d3.select("body").append("svg")
      .attr("width", width)
      .attr("height", height);

    var data = [
      { name: "A"},
      { name: "B"},
      { name: "C"}
    ];

    var rect = Rect()
      .colorBy("name")
      .color(d3.scale.category10())

    var facetVertical = FacetVertical()
      .width(width)
      .height(height)
      .nestBy("name");
    
    // Define the composition of facet and rect.
    facetVertical.compose(rect);

    svg.datum(data).call(facetVertical);

    // This component draws a single rectangle.
    function Rect(){
      var color;
      var colorBy;

      function my(selection){
        selection.each(function (data){

          var rects = d3.select(this)
            .selectAll("rect")
            .data(data);

          rects.enter().append("rect");
          
          rects
            .attr("width", width)
            .attr("height", height);

          if(colorBy){
            rects.attr("fill", function (d){
              return color(d[colorBy]);
            });
          }

          rects.exit().remove();
        });
      }

      my.width = function (value){
        if(arguments.length === 0) return width;
        width = value;
        return my;
      };
      my.height = function (value){
        if(arguments.length === 0) return height;
        height = value;
        return my;
      };
      my.color = function (value){
        if(arguments.length === 0) return color;
        color = value;
        return my;
      };
      my.colorBy = function (value){
        if(arguments.length === 0) return colorBy;
        colorBy = value;
        return my;
      };
      return my;
    }

    // This component facets space vertically,
    // and could be used to create small multiples.
    function FacetVertical(){
      var width;
      var height;

      var compose;
      var nestBy;

      function my(selection){
        selection.each(function (data){

          var nested = d3.nest()
            .key(function (d){ return d[nestBy]; })
            .entries(data)
            .map(function (d){ return d.values; });

          var groups = d3.select(this)
            .selectAll("g")
            .data(nested)

          groups.enter().append("g");
          groups.attr("transform", function (d, i){
            var y = height * i / nested.length;
            return "translate(0," + y + ")";
          });

          groups.exit().remove();

          compose.width(width);
          compose.height(height / nested.length);

          groups.call(compose);
        });
      }

      my.width = function (value){
        if(arguments.length === 0) return width;
        width = value;
        return my;
      };
      my.height = function (value){
        if(arguments.length === 0) return height;
        height = value;
        return my;
      };
      my.compose = function (value){
        if(arguments.length === 0) return compose;
        compose = value;
        return my;
      };
      my.nestBy = function (value){
        if(arguments.length === 0) return nestBy;
        nestBy = value;
        return my;
      };
      return my;
    }

  </script>
</body>