Skip to content

Instantly share code, notes, and snippets.

@curran
Last active August 29, 2015 14:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save curran/35b5ec7ad6547cd95781 to your computer and use it in GitHub Desktop.
Save curran/35b5ec7ad6547cd95781 to your computer and use it in GitHub Desktop.
Generalizing D3 patterns ad infinitum

This is a scatter plot of the Iris data set.

This is an experiment to see how far one can go in generalizing D3 visualization patterns. The main file of interest here is reactivis.js. For example, a single code path creates all scales (x, y, size, color) and sets up their reactive dependencies. This is a proof-of-concept for a larger scale project that aims to provide a base layer for many different D3 visualizations.

Notice that if you open this in a new window, it responds when you resize the browser window. This is coded in such a way that the visualization size can be controlled via CSS.

The code for this is derived from example 106 of the screencast Introduction to D3.js.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>D3 Example</title>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="model-min.js"></script>
<script src="reactivis.js"></script>
<script src="scatterPlot.js"></script>
<script src="barChart.js"></script>
<link href='http://fonts.googleapis.com/css?family=Poiret+One' rel='stylesheet' type='text/css'>
<style>
body {
background-color: lightgray;
}
.axis text {
font-family: 'Poiret One', cursive;
font-size: 16pt;
}
.axis .label {
font-size: 20pt;
}
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.visualization {
position: fixed;
top: 50px;
bottom: 50px;
left: 280px;
right: 280px;
background-color: white;
border-radius: 25px;
border-style: dotted;
border-width: 2px;
}
</style>
</head>
<body>
<div class="visualization" id="scatterPlotContainer"></div>
<script>
function setupScatterPlot(){
var container = d3.select("#scatterPlotContainer");
var scatterPlot = ScatterPlot();
scatterPlot.set({
outerWidth: 100,
outerHeight: 500,
margin: { left: 80, top: 10, right: 15, bottom: 80 },
rMin: 2, // "r" stands for radius
rMax: 15,
xColumn: "sepal_length",
yColumn: "petal_length",
rColumn: "sepal_width",
colorColumn: "species",
colorRange: d3.scale.category10().range(),
xAxisLabelText: "Sepal Length (cm)",
xAxisLabelOffset: 65,
xAxisTicks: 10,
xAxisTickFormat: d3.format("s"),
yAxisLabelText: "Petal Length (cm)",
yAxisLabelOffset: 35,
yAxisTicks: 5,
yAxisTickFormat: d3.format("s"),
container: container
});
function type(d){
d.sepal_length = +d.sepal_length;
d.sepal_width = +d.sepal_width;
d.petal_length = +d.petal_length;
d.petal_width = +d.petal_width;
return d;
}
d3.csv("iris.csv", type, function (data) {
scatterPlot.data = data;
});
}
setupScatterPlot();
</script>
</body>
</html>
sepal_length sepal_width petal_length petal_width species
5.1 3.5 1.4 0.2 setosa
4.9 3.0 1.4 0.2 setosa
4.7 3.2 1.3 0.2 setosa
4.6 3.1 1.5 0.2 setosa
5.0 3.6 1.4 0.2 setosa
5.4 3.9 1.7 0.4 setosa
4.6 3.4 1.4 0.3 setosa
5.0 3.4 1.5 0.2 setosa
4.4 2.9 1.4 0.2 setosa
4.9 3.1 1.5 0.1 setosa
5.4 3.7 1.5 0.2 setosa
4.8 3.4 1.6 0.2 setosa
4.8 3.0 1.4 0.1 setosa
4.3 3.0 1.1 0.1 setosa
5.8 4.0 1.2 0.2 setosa
5.7 4.4 1.5 0.4 setosa
5.4 3.9 1.3 0.4 setosa
5.1 3.5 1.4 0.3 setosa
5.7 3.8 1.7 0.3 setosa
5.1 3.8 1.5 0.3 setosa
5.4 3.4 1.7 0.2 setosa
5.1 3.7 1.5 0.4 setosa
4.6 3.6 1.0 0.2 setosa
5.1 3.3 1.7 0.5 setosa
4.8 3.4 1.9 0.2 setosa
5.0 3.0 1.6 0.2 setosa
5.0 3.4 1.6 0.4 setosa
5.2 3.5 1.5 0.2 setosa
5.2 3.4 1.4 0.2 setosa
4.7 3.2 1.6 0.2 setosa
4.8 3.1 1.6 0.2 setosa
5.4 3.4 1.5 0.4 setosa
5.2 4.1 1.5 0.1 setosa
5.5 4.2 1.4 0.2 setosa
4.9 3.1 1.5 0.1 setosa
5.0 3.2 1.2 0.2 setosa
5.5 3.5 1.3 0.2 setosa
4.9 3.1 1.5 0.1 setosa
4.4 3.0 1.3 0.2 setosa
5.1 3.4 1.5 0.2 setosa
5.0 3.5 1.3 0.3 setosa
4.5 2.3 1.3 0.3 setosa
4.4 3.2 1.3 0.2 setosa
5.0 3.5 1.6 0.6 setosa
5.1 3.8 1.9 0.4 setosa
4.8 3.0 1.4 0.3 setosa
5.1 3.8 1.6 0.2 setosa
4.6 3.2 1.4 0.2 setosa
5.3 3.7 1.5 0.2 setosa
5.0 3.3 1.4 0.2 setosa
7.0 3.2 4.7 1.4 versicolor
6.4 3.2 4.5 1.5 versicolor
6.9 3.1 4.9 1.5 versicolor
5.5 2.3 4.0 1.3 versicolor
6.5 2.8 4.6 1.5 versicolor
5.7 2.8 4.5 1.3 versicolor
6.3 3.3 4.7 1.6 versicolor
4.9 2.4 3.3 1.0 versicolor
6.6 2.9 4.6 1.3 versicolor
5.2 2.7 3.9 1.4 versicolor
5.0 2.0 3.5 1.0 versicolor
5.9 3.0 4.2 1.5 versicolor
6.0 2.2 4.0 1.0 versicolor
6.1 2.9 4.7 1.4 versicolor
5.6 2.9 3.6 1.3 versicolor
6.7 3.1 4.4 1.4 versicolor
5.6 3.0 4.5 1.5 versicolor
5.8 2.7 4.1 1.0 versicolor
6.2 2.2 4.5 1.5 versicolor
5.6 2.5 3.9 1.1 versicolor
5.9 3.2 4.8 1.8 versicolor
6.1 2.8 4.0 1.3 versicolor
6.3 2.5 4.9 1.5 versicolor
6.1 2.8 4.7 1.2 versicolor
6.4 2.9 4.3 1.3 versicolor
6.6 3.0 4.4 1.4 versicolor
6.8 2.8 4.8 1.4 versicolor
6.7 3.0 5.0 1.7 versicolor
6.0 2.9 4.5 1.5 versicolor
5.7 2.6 3.5 1.0 versicolor
5.5 2.4 3.8 1.1 versicolor
5.5 2.4 3.7 1.0 versicolor
5.8 2.7 3.9 1.2 versicolor
6.0 2.7 5.1 1.6 versicolor
5.4 3.0 4.5 1.5 versicolor
6.0 3.4 4.5 1.6 versicolor
6.7 3.1 4.7 1.5 versicolor
6.3 2.3 4.4 1.3 versicolor
5.6 3.0 4.1 1.3 versicolor
5.5 2.5 4.0 1.3 versicolor
5.5 2.6 4.4 1.2 versicolor
6.1 3.0 4.6 1.4 versicolor
5.8 2.6 4.0 1.2 versicolor
5.0 2.3 3.3 1.0 versicolor
5.6 2.7 4.2 1.3 versicolor
5.7 3.0 4.2 1.2 versicolor
5.7 2.9 4.2 1.3 versicolor
6.2 2.9 4.3 1.3 versicolor
5.1 2.5 3.0 1.1 versicolor
5.7 2.8 4.1 1.3 versicolor
6.3 3.3 6.0 2.5 virginica
5.8 2.7 5.1 1.9 virginica
7.1 3.0 5.9 2.1 virginica
6.3 2.9 5.6 1.8 virginica
6.5 3.0 5.8 2.2 virginica
7.6 3.0 6.6 2.1 virginica
4.9 2.5 4.5 1.7 virginica
7.3 2.9 6.3 1.8 virginica
6.7 2.5 5.8 1.8 virginica
7.2 3.6 6.1 2.5 virginica
6.5 3.2 5.1 2.0 virginica
6.4 2.7 5.3 1.9 virginica
6.8 3.0 5.5 2.1 virginica
5.7 2.5 5.0 2.0 virginica
5.8 2.8 5.1 2.4 virginica
6.4 3.2 5.3 2.3 virginica
6.5 3.0 5.5 1.8 virginica
7.7 3.8 6.7 2.2 virginica
7.7 2.6 6.9 2.3 virginica
6.0 2.2 5.0 1.5 virginica
6.9 3.2 5.7 2.3 virginica
5.6 2.8 4.9 2.0 virginica
7.7 2.8 6.7 2.0 virginica
6.3 2.7 4.9 1.8 virginica
6.7 3.3 5.7 2.1 virginica
7.2 3.2 6.0 1.8 virginica
6.2 2.8 4.8 1.8 virginica
6.1 3.0 4.9 1.8 virginica
6.4 2.8 5.6 2.1 virginica
7.2 3.0 5.8 1.6 virginica
7.4 2.8 6.1 1.9 virginica
7.9 3.8 6.4 2.0 virginica
6.4 2.8 5.6 2.2 virginica
6.3 2.8 5.1 1.5 virginica
6.1 2.6 5.6 1.4 virginica
7.7 3.0 6.1 2.3 virginica
6.3 3.4 5.6 2.4 virginica
6.4 3.1 5.5 1.8 virginica
6.0 3.0 4.8 1.8 virginica
6.9 3.1 5.4 2.1 virginica
6.7 3.1 5.6 2.4 virginica
6.9 3.1 5.1 2.3 virginica
5.8 2.7 5.1 1.9 virginica
6.8 3.2 5.9 2.3 virginica
6.7 3.3 5.7 2.5 virginica
6.7 3.0 5.2 2.3 virginica
6.3 2.5 5.0 1.9 virginica
6.5 3.0 5.2 2.0 virginica
6.2 3.4 5.4 2.3 virginica
5.9 3.0 5.1 1.8 virginica
// This file comes from https://github.com/curran/model
!function(){function n(n){function t(n,t,r){r=r||this,n=n instanceof Array?n:[n];var u=o(function(){var o=n.map(function(n){return p[n]});e(o)&&t.apply(r,o)});return u(),n.forEach(function(n){f(n,u)}),u}function o(n){var t=!1;return function(){t||(t=!0,setTimeout(function(){t=!1,n()},0))}}function e(n){return!n.some(function(n){return"undefined"==typeof n||null===n})}function f(n,t,o){o=o||this,r(n).push(t),u(n,o)}function r(n){return d[n]||(d[n]=[])}function u(n,t){n in l||(l[n]=!0,p[n]=s[n],Object.defineProperty(s,n,{get:function(){return p[n]},set:function(o){var e=p[n];p[n]=o,r(n).forEach(function(n){n.call(t,o,e)})}}))}function i(n){for(var t in d)c(t,n)}function c(n,t){d[n]=d[n].filter(function(n){return n!==t})}function a(n){for(var t in n)s[t]=n[t]}var s={},p={},d={},l={};return a(n),s.when=t,s.cancel=i,s.on=f,s.off=c,s.set=a,s}n.None="__NONE__","function"==typeof define&&define.amd?define([],function(){return n}):"object"==typeof exports?module.exports=n:this.Model=n}();
function Reactivis(model){
var scales = {
linear: d3.scale.linear,
ordinal: d3.scale.ordinal
};
function scale(prefix, type){
type = type || "linear";
var scale = scales[type]();
var columnProperty = prefix + "Column";
var accessorProperty = prefix + "Accessor";
var domainProperty = prefix + "Domain";
var rangeProperty = prefix + "Range";
var scaleProperty = prefix + "Scale";
model.when(columnProperty, function (column){
model[accessorProperty] = get(column);
});
model.when(["data", accessorProperty], function(data, accessor){
if(type === "linear"){
model[domainProperty] = d3.extent(data, accessor);
} else if(type === "ordinal"){
model[domainProperty] = data.map(accessor);
}
});
model.when([domainProperty, rangeProperty], function (domain, range){
model[scaleProperty] = scale
.domain(domain)
.range(range);
});
model.when([scaleProperty, accessorProperty], function (scale, accessor){
model[prefix] = compose(scale, accessor);
});
}
function axis(prefix){
var axis = d3.svg.axis()
.outerTickSize(0);
var scaleProperty = prefix + "Scale";
var axisProperty = prefix + "Axis";
var axisGProperty = axisProperty + "G";
var ticksProperty = axisProperty + "Ticks";
var labelProperty = axisProperty + "Label";
var textProperty = labelProperty + "Text";
var tickFormatProperty = axisProperty + "TickFormat";
model.when([scaleProperty, ticksProperty, tickFormatProperty],
function(scale, ticks, tickFormat){
model[axisProperty] = axis
.scale(scale)
.ticks(ticks)
.tickFormat(tickFormat);
});
model.when([axisGProperty, axisProperty], function (axisG, axis){
axisG.call(axis);
});
model.when("g", function (g){
var axisG = g.append("g")
.attr("class", prefix + " axis");
model[axisGProperty] = axisG;
model[labelProperty] = axisG.append("text")
.style("text-anchor", "middle")
.attr("class", "label");
});
model.when([labelProperty, textProperty], function(label, text){
label.text(text);
});
return axis;
}
// http://en.wikipedia.org/wiki/Function_composition
function compose(g, f){
return function(d){ return g(f(d)); };
}
// Abstracts the common pattern of accessing an object property.
function get(property){
return function(d){ return d[property]; };
}
var reactivis = {
svg: function (){
model.when("container", function (container) {
model.svg = container.append("svg");
});
model.when(["svg", "outerWidth"], function(svg, outerWidth){
svg.attr("width", outerWidth);
});
model.when(["svg", "outerHeight"], function(svg, outerHeight){
svg.attr("height", outerHeight);
});
model.when("svg", function (svg){
model.g = svg.append("g");
});
model.when(["g", "margin"], function (g, margin){
g.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
});
return reactivis;
},
// This encapsulates the D3 margin convention from http://bl.ocks.org/mbostock/3019563
margin: function (){
model.when(["outerWidth", "margin"], function(outerWidth, margin){
model.innerWidth = outerWidth - margin.left - margin.right;
});
model.when(["outerHeight", "margin"], function(outerHeight, margin){
model.innerHeight = outerHeight - margin.top - margin.bottom
});
return reactivis;
},
xScale: function (type){
scale("x", type);
model.when("innerWidth", function (innerWidth){
model.xRange = [0, innerWidth];
});
return reactivis;
},
yScale: function (type){
scale("y", type);
model.when("innerHeight", function (innerHeight){
model.yRange = [innerHeight, 0];
});
return reactivis;
},
xAxis: function (){
axis("x").orient("bottom");
model.when(["xAxisG", "innerHeight"], function (xAxisG, innerHeight){
xAxisG.attr("transform", "translate(0," + innerHeight + ")");
});
model.when(["xAxisLabel", "innerWidth"], function(xAxisLabel, innerWidth){
xAxisLabel.attr("x", innerWidth / 2);
});
model.when(["xAxisLabel", "xAxisLabelOffset"], function(xAxisLabel, xAxisLabelOffset){
xAxisLabel.attr("y", xAxisLabelOffset)
});
return reactivis;
},
yAxis: function (){
axis("y").orient("left");
model.when(["yAxisLabel", "innerHeight", "yAxisLabelOffset"],
function(yAxisLabel, innerHeight, yAxisLabelOffset){
yAxisLabel
.attr("transform", "translate(-" + yAxisLabelOffset + "," + (innerHeight / 2) + ") rotate(-90)")
});
return reactivis;
},
rScale: function (){
scale("r");
model.when(["rMin", "rMax"], function (rMin, rMax){
model.rRange = [rMin, rMax];
});
return reactivis;
},
colorScale: function (){
scale("color", "ordinal");
return reactivis;
},
resize: function (){
model.when("container", function (container){
function setSize(){
var containerNode = container.node();
model.set({
outerWidth: containerNode.clientWidth,
outerHeight: containerNode.clientHeight
});
}
d3.select(window).on("resize", setSize);
setSize();
});
}
};
return reactivis;
}
function ScatterPlot(){
var model = Model();
Reactivis(model)
.svg()
.margin()
.xScale().xAxis()
.yScale().yAxis()
.rScale()
.colorScale()
.resize();
model.when(["g", "data", "x", "y", "r", "color"], function(g, data, x, y, r, color){
var circles = g.selectAll("circle").data(data);
circles.enter().append("circle");
circles
.attr("cx", x)
.attr("cy", y)
.attr("r", r)
.attr("fill", color)
circles.exit().remove();
});
return model;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment