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
.
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
xxxxxxxxxx
<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>
https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js