Reusable
Visualizations
Miles McCrocklin
@milr0c
Reusable Visualizations API
Backend MV*
Frontend MV* (Brief)
Resources
data = randomizeData(20, Math.random()*100000);
margin = {top: 0, bottom: 20, left: 0, right: 0},
width = 400,
height = 400,
duration = 500,
formatNumber = d3.format(',d'),
brush = d3.svg.brush();
margin.left = formatNumber(d3.max(data, function(d) { return d.y; })).length * 14;
w = width - margin.left - margin.right,
h = height - margin.top - margin.bottom;
x = d3.scale.ordinal()
.rangeRoundBands([0, w], .1),
y = d3.scale.linear()
.range([h, 0]);
y.domain([0, d3.max(data, function(d) { return d.y; })]);
x.domain(data.map(function(d) { return d.x; }));
xAxis = d3.svg.axis()
.scale(x)
.orient('bottom'),
yAxis = d3.svg.axis()
.scale(y)
.orient('left');
svg = d3.select('#chart').selectAll('svg').data([data]),
svgEnter = svg.enter().append('svg')
.append('g')
.attr('width', w)
.attr('height', h)
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.classed('chart', true),
chart = d3.select('.chart');
svgEnter.append('g')
.classed('x axis', true)
.attr('transform', 'translate(' + 0 + ',' + h + ')');
svgEnter.append('g')
.classed('y axis', true)
svgEnter.append('g').classed('barGroup', true);
chart.selectAll('.brush').remove();
chart.selectAll('.selected').classed('selected', false);
chart.append('g')
.classed('brush', true)
.call(brush)
.selectAll('rect')
.attr('height', h);
bars = chart.select('.barGroup').selectAll('.bar').data(data);
bars.enter()
.append('rect')
.classed('bar', true)
.attr('x', w) // start here for object constancy
.attr('width', x.rangeBand())
.attr('y', function(d, i) { return y(d.y); })
.attr('height', function(d, i) { return h - y(d.y); });
bars.attr('width', x.rangeBand())
.attr('x', function(d, i) { return x(d.x); })
.attr('y', function(d, i) { return y(d.y); })
.attr('height', function(d, i) { return h - y(d.y); });
bars.exit().style('opacity', 0).remove();
chart.select('.x.axis').call(xAxis);
chart.select('.y.axis').call(yAxis);
var bar = charts.bar()
.width(600)
.height(600);
var data = [{x: 0, y: 30},
{x: 1, y: 100},
{x: 2, y: 60},
{x:3, y:2}];
d3.select('#DIV-ID')
.datum(data)
.call(bar);
charts.bar = function() {
function render(selection) {
// render chart
}
// accessors
return render;
};
var bar = charts.bar();
var data = [{x: 0, y: 30}, {x: 1, y: 100},
{x: 2, y: 60}, {x:3, y:2}];
d3.select('#DIV-ID')
.call(bar);
var bar = charts.bar()
.width(200);
var data = [{x: 0, y: 30}, {x: 1, y: 100},
{x: 2, y: 60}, {x:3, y:2}];
d3.select('#DIV-ID')
.datum(data)
.call(bar);
Closure
closes over the free variables (variables which are not local variables)
charts.bar = function() {
var width = 800;
function render(selection) {
// render chart
}
// accessors
return render;
};
charts.bar = function() {
var width = 800;
function render(selection) {
// render chart
}
// accessors
render.width = function(val) {
if(!arguments.length) return width;
width = val;
return render;
};
return render;
};
// basic data
var margin = {top: 20, bottom: 20, left: 0, right: 0},
width = 400,
height = 400,
// accessors
xValue = function(d) { return d.x; },
yValue = function(d) { return d.y; },
// chart underpinnings
brush = d3.svg.brush(),
xAxis = d3.svg.axis().orient('bottom'),
yAxis = d3.svg.axis().orient('left'),
x = d3.scale.ordinal(),
y = d3.scale.linear(),
// chart enhancements
duration = 500,
formatNumber = d3.format(',d');
var bar = charts.bar();
var data = [{x: 0, y: 30}, {x: 1, y: 100},
{x: 2, y: 60}, {x:3, y:2}];
d3.select('#DIV-ID')
.datum(data)
.call(bar);
charts.bar = function() {
var width = 800;
function render(selection) {
// render chart
}
// accessors
render.width = function(val) {
if(!arguments.length) return width;
width = val;
return render;
};
return render;
};
var bar = charts.bar()
.xValue(function(d) { return d.id; })
.yValue(function(d) { return d.value; });
var data = [{id: 0, value: 30}, {id: 1, value: 100},
{id: 2, value: 60}, {id:3, value:2}];
d3.select('#DIV-ID')
.datum(data)
.call(bar);
function render(selection) {
// selection has __data__ = data, no join.
selection.each(function(data) {
// if needed, convert to standard representation of data
data = data.map(function(d, i) {
return [xValue.call(data, d, i),
yValue.call(data, d, i)];
});
// create skeleton chart
// visualize
});
}
function render(selection) {
// selection has __data__ = data, no join.
selection.each(function(data) {
// if needed, convert to standard representation of data
data = data.map(function(d, i) {
return [xValue.call(data, d, i),
yValue.call(data, d, i)];
});
// create skeleton chart
var svg = d3.select(this)
.selectAll('svg')
.data([data]);
var gEnter = svg.enter().append("svg").append("g");
// visualize
});
}
// visualize
// reset closure'd values
var w = width - margin.left - margin.right;
x.domain(data.map(function(d) { return d.x; }))
.rangeRoundBands([0, w], .1);
// visualize
// render the bars
var bars = chart.select('.barGroup')
.selectAll('.bar')
.data(data);
bars.enter().append('rect') // new data
.classed('bar', true);
bars.style('opacity', 1) // new data and updating current data
.attr('width', x.rangeBand())
.attr('x', function(d, i) { return x(d.x); })
.attr('y', function(d, i) { return y(d.y); })
.attr('height', function(d, i) { return h - y(d.y); });
bars.exit().style('opacity', 0).remove(); // removed data
charts.bar = function() {
function render(selection) {
// render chart
}
// accessors
d3.rebind(render, brush, 'on');
return render;
};
var bar = charts.bar()
.on('brushstart', sum)
.on('brushend', sum)
.on('brush', sum);
var data = [{x: 0, y: 30}, {x: 1, y: 100},
{x: 2, y: 60}, {x:3, y:2}];
d3.select('#DIV-ID')
.datum(data)
.call(bar);
function sum() {
var extent = d3.event.target.extent();
var x = bar.x();
// sum the elements in extent
}
var data;
d3.json('/controller/call/get/model', function(json) {
data = json;
main();
});
function main() {
var bar = charts.bar();
d3.select('#chart')
.datum(data)
.call(bar);
}
Keep data visualization code separate from your MVWhatever code
D3 is intentionally a low-level system. During the early design of D3, we even referred to it as a "visualization kernel" rather than a "toolkit" or "framework".
Ember.js is the backbone of our new analytics page, while D3.js – to continue the tortured analogy – is the muscle powering our visualizations.
main.directive('chartBar', function() {
var bar = charts.bar();
return {
restrict: 'E',
replace: true,
template: '<div class="chart"></div>',
scope: {
data: '=',
},
link: function($scope, $element, $attr) {
$scope.$watch('data', function(newVal, oldVal) {
d3.select($element[0]).datum(newVal).call(bar);
});
}
}
});
<chart-bar height={{height}} width={{width}} data={{data}}>
</chart-bar>
var BarChart = Backbone.View.extend({
el: "#chart",
initialize: function() {
_.bindAll(this, "render");
this.collection.bind("change add remove", this.render);
var bar = this.bar = charts.bar();
},
render: function() {
var data = this.collection.models;
d3.select(this.$el.selector)
.datum(data)
.call(this.bar);
},
});
App.barView = Ember.View.extend({
update: function() {
var elementId = this.get('elementId');
var data = this.get('content');
var bar = this.get('bar') || charts.bar();
d3.select('#'+elementId)
.datum(data)
.call(bar);
this.set('bar', bar);
}.observes('content@each.value'),
didInsertElement: function() {
this.update();
}
});
*crossfilter.js