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/198f19dbfdf071390ee1 to your computer and use it in GitHub Desktop.
Save curran/198f19dbfdf071390ee1 to your computer and use it in GitHub Desktop.
The Reactivis Concept

This is a scatter plot of the Iris data set.

The purpose of this example is to show one approach to isolate reusable reactive flows that are reusable between visualizations, in an API called Reactivis, which stands for "reactive visualizations". This approach allows one to generalize D3 patterns ad infinitum.

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>
<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;
}
circle:hover {
stroke-width: 2px;
stroke: black;
}
</style>
</head>
<body>
<div class="visualization" id="scatterPlotContainer"></div>
<script>
function Reactivis(model){
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 (){
var xScale = d3.scale.linear();
model.when("xColumn", function (xColumn){
model.xAccessor = get(xColumn);
});
model.when(["data", "xAccessor"], function(data, xAccessor){
model.xDomain = d3.extent(data, xAccessor);
});
model.when("innerWidth", function (innerWidth){
model.xRange = [0, innerWidth];
});
model.when(["xDomain", "xRange"], function (xDomain, xRange){
model.xScale = xScale
.domain(xDomain)
.range(xRange);
});
model.when(["xScale", "xAccessor"], function (xScale, xAccessor){
model.x = compose(xScale, xAccessor);
});
return reactivis;
},
xAxis: function (){
var xAxis = d3.svg.axis()
.orient("bottom")
.tickFormat(d3.format("s"))
.outerTickSize(0);
model.when(["xScale", "xAxisNumTicks"], function (xScale, xAxisNumTicks){
model.xAxis = xAxis
.scale(xScale)
.ticks(xAxisNumTicks);
});
model.when(["xAxisG", "xAxis"], function (xAxisG, xAxis){
xAxisG.call(xAxis);
});
model.when("g", function (g){
model.xAxisG = g.append("g")
.attr("class", "x axis");
model.xAxisLabel = model.xAxisG.append("text")
.style("text-anchor", "middle")
.attr("class", "label");
});
model.when(["xAxisG", "innerHeight"], function (xAxisG, innerHeight){
xAxisG.attr("transform", "translate(0," + innerHeight + ")");
});
model.when(["xAxisLabel", "innerWidth", "xAxisLabelOffset", "xAxisLabelText"],
function(xAxisLabel, innerWidth, xAxisLabelOffset, xAxisLabelText){
xAxisLabel
.attr("x", innerWidth / 2)
.attr("y", xAxisLabelOffset)
.text(xAxisLabelText);
});
return reactivis;
},
yScale: function (){
var yScale = d3.scale.linear();
model.when("yColumn", function(yColumn){
model.yAccessor = get(yColumn);
});
model.when(["data", "yAccessor"], function(data, yAccessor){
model.yDomain = d3.extent(data, yAccessor);
});
model.when("innerHeight", function (innerHeight){
model.yRange = [innerHeight, 0];
});
model.when(["yDomain", "yRange"], function (yDomain, yRange){
model.yScale = yScale
.domain(yDomain)
.range(yRange);
});
model.when(["yScale", "yAccessor"], function (yScale, yAccessor){
model.y = compose(yScale, yAccessor);
});
return reactivis;
},
yAxis: function (){
var yAxis = d3.svg.axis()
.orient("left")
.tickFormat(d3.format("s"))
.outerTickSize(0);
model.when(["yScale", "yAxisNumTicks"], function (yScale, yAxisNumTicks){
model.yAxis = yAxis.scale(yScale).ticks(yAxisNumTicks);
});
model.when(["yAxisG", "yAxis"], function (yAxisG, yAxis){
yAxisG.call(yAxis);
});
model.when("g", function (g){
model.yAxisG = g.append("g")
.attr("class", "y axis");
model.yAxisLabel = model.yAxisG.append("text")
.style("text-anchor", "middle")
.attr("class", "label");
});
model.when(["yAxisLabel", "innerHeight", "yAxisLabelOffset", "yAxisLabelText"],
function(yAxisLabel, innerHeight, yAxisLabelOffset, yAxisLabelText){
yAxisLabel
.attr("transform", "translate(-" + yAxisLabelOffset + "," + (innerHeight / 2) + ") rotate(-90)")
.text(yAxisLabelText);
});
return reactivis;
},
rScale: function (){
var rScale = d3.scale.linear();
model.when("rColumn", function(rColumn){
model.rAccessor = get(rColumn);
});
model.when(["data", "rAccessor"], function(data, rAccessor){
model.rDomain = d3.extent(data, rAccessor);
});
model.when(["rMin", "rMax"], function (rMin, rMax){
model.rRange = [rMin, rMax];
});
model.when(["rDomain", "rRange"], function (rDomain, rRange){
model.rScale = rScale
.domain(rDomain)
.range(rRange);
});
model.when(["rScale", "rAccessor"], function (rScale, rAccessor){
model.r = compose(rScale, rAccessor);
});
return reactivis;
},
colorScale: function (){
var colorScale = d3.scale.ordinal();
model.when("colorColumn", function(colorColumn){
model.colorAccessor = get(colorColumn);
});
model.when(["data", "colorAccessor"], function(data, colorAccessor){
model.colorDomain = data.map(colorAccessor);
});
model.when(["colorDomain", "colorRange"], function (colorDomain, colorRange){
model.colorScale = colorScale
.domain(colorDomain)
.range(colorRange);
});
model.when(["colorScale", "colorAccessor"], function (colorScale, colorAccessor){
model.color = compose(colorScale, colorAccessor);
});
return reactivis;
}
};
return reactivis;
}
// 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]; };
}
function ScatterPlot(){
var model = Model();
Reactivis(model)
.svg()
.margin()
.xScale().xAxis()
.yScale().yAxis()
.rScale()
.colorScale();
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;
}
(function main(){
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,
xAxisNumTicks: 10,
yAxisLabelText: "Petal Length (cm)",
yAxisLabelOffset: 35,
yAxisNumTicks: 5,
container: container
});
d3.csv("iris.csv", type, function (data) {
scatterPlot.data = data;
});
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;
}
// Allow CSS to drive visualization size.
function setSize(){
var containerNode = container.node();
scatterPlot.set({
outerWidth: containerNode.clientWidth,
outerHeight: containerNode.clientHeight
});
}
d3.select(window)
.on("load" , setSize)
.on("resize", setSize);
}());
</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}();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment