Skip to content

Instantly share code, notes, and snippets.

@biovisualize
Created May 8, 2014 04:41
Show Gist options
  • Save biovisualize/eaa17e4f9b72c3cc6145 to your computer and use it in GitHub Desktop.
Save biovisualize/eaa17e4f9b72c3cc6145 to your computer and use it in GitHub Desktop.
visualization pipeline library
// Utils
/////////////////////////
//Modified from http://scott.sauyet.com/Javascript/Talk/Compose/2013-05-22/#slide-28
var pipelineStart = (function(){
var chain = function(fn){
var f1 = function(g){
var func = function(){
return g.call(this, fn.apply(this, arguments));};
chain(func);
return func;
};
var f2 = function(g){
return function(){ return g.call(this, fn.apply(this, arguments));}; };
fn.then = f1;
fn.pipelineEnd = f2;
};
return function(f){
var fn = function(){ return f.apply(this, arguments); };
chain(fn);
return fn;
};
}());
function deepExtend(destination, source) {
for (var property in source) {
if (source[property] && source[property].constructor &&
source[property].constructor === Object) {
destination[property] = destination[property] || {};
arguments.callee(destination[property], source[property]);
} else {
destination[property] = source[property];
}
}
return destination;
}
// Visualization pipes
/////////////////////////
// Initialize the chart configuration and template
function init(config){
var defaultConfig = {
container: null,
data: null,
width: 800,
height: 200,
margin: {top: 60, right: 60, bottom: 20, left: 20}
};
config = deepExtend(defaultConfig, config);
config.cache = {
root: null,
data: null,
chartW: config.width - config.margin.left - config.margin.right,
chartH: config.height - config.margin.top - config.margin.bottom
};
var template = '<div class="chart"><svg>' +
'<g class="chart-panel">' +
'<g class="background"><rect class="bg-rect"></rect></g>' +
'<g class="axis-x"></g>' +
'<g class="axis-y"></g>' +
'<g class="geometry"></g>' +
'<g class="hover"></g>' +
'</g>' +
'</svg></div>';
config.cache.root = d3.select(config.container)
.html(template)
.select('svg')
.attr({
height: config.height,
width: config.width
});
config.cache.root.select('.chart-panel')
.attr({transform: 'translate('+[config.margin.left, config.margin.top]+')'})
return config;
}
// Prepare the data
function prepareData(config){
config.cache.data = config.data;
return config;
}
// Build the scales
function prepareCatNumScales(config){
config.cache.scaleX = d3.scale.ordinal()
.domain(config.cache.data[0].x)
.rangeBands([0, config.cache.chartW]);
config.cache.scaleY = d3.scale.linear()
.domain([0, d3.max(d3.merge(config.cache.data.map(function(d){ return d.y; })))])
.range([0, config.cache.chartH]);
config.cache.dataPixelSpace = config.cache.data.map(function(d, i){
return d.x.map(function(dB, iB){ return {x: config.cache.scaleX(dB), y: config.cache.scaleY(d.y[iB])}; });
});
return config;
}
function prepareNumNumScales(config){
config.cache.scaleX = d3.scale.linear()
.domain([0, d3.max(d3.merge(config.cache.data.map(function(d){ return d.x; })))])
.range([0, config.cache.chartW]);
config.cache.scaleY = d3.scale.linear()
.domain([0, d3.max(d3.merge(config.cache.data.map(function(d){ return d.y; })))])
.range([0, config.cache.chartH]);
config.cache.dataPixelSpace = config.cache.data.map(function(d, i){
return d.x.map(function(dB, iB){ return {x: config.cache.scaleX(dB), y: config.cache.scaleY(d.y[iB])}; });
});
return config;
}
function prepareNumCatScales(config){
config.cache.scaleX = d3.scale.linear()
.domain(d3.extent(config.cache.data.y))
.range([0, config.cache.chartW]);
config.cache.scaleY = d3.scale.ordinal()
.domain(config.cache.data.x)
.rangeBands([0, config.cache.chartH]);
config.cache.dataPixelSpace = config.cache.data.x.map(function(d, i){
return {x: config.cache.scaleX(config.cache.data.y[i]), y: config.cache.scaleY(d)};
});
return config;
}
// Render the axes
function renderAxis(config){
config = renderBackground(config);
config = renderStripes(config);
var scaleY = config.cache.scaleY.copy();
scaleY.range(scaleY.range().reverse());
var axisX = d3.svg.axis()
.scale(config.cache.scaleX);
config.cache.root.select('g.axis-x')
.attr({transform: 'translate('+[0, config.cache.chartH + 1]+')'})
.call(axisX)
.selectAll('path.domain').style({fill: 'none'});
var axisY = d3.svg.axis()
.scale(scaleY)
.orient('left');
config.cache.root.select('g.axis-y')
.attr({transform: 'translate('+[-1, 0]+')'})
.call(axisY)
.selectAll('path.domain').style({fill: 'none'});
return cleanupAxis(config);
}
function renderBackground(config){
config.cache.root.select('.background .bg-rect')
.attr({
height: config.cache.chartH,
width: config.cache.chartW
});
return config;
}
function renderStripes(config){
if(!config.cache.scaleX.rangeBand) return config;
var stripeSpan = 2;
var barGroupW = config.cache.scaleX.rangeBand() * stripeSpan;
var attr = {
width: barGroupW,
height: config.cache.chartH,
x: function(d, i, pI){ return i * barGroupW; },
y: 0
};
var markSelection = config.cache.root.select('.background').selectAll('rect.stripe')
.data(config.cache.dataPixelSpace[0].filter(function(d, i){ return !!(i%stripeSpan); }));
markSelection.enter().append('rect').attr({'class': 'stripe'});
markSelection.attr(attr)
.each(function(d, i, pI){ d3.select(this).classed('stripe' + (i%stripeSpan), true); });
markSelection.exit().remove();
return config;
}
function cleanupAxis(config){
var ticksX = config.cache.root.select('g.axis-x').selectAll('.tick');
var ticksMaxW = d3.max(ticksX[0].map(function(d){ return d.getBBox().width; }));
var divisor = Math.ceil(ticksMaxW / (config.cache.chartW / ticksX[0].length));
ticksX.filter(function(d, i){ return i%divisor !== 0; })
.style({display: 'none'});
var ticksY = config.cache.root.select('g.axis-y').selectAll('.tick');
var ticksMaxH = d3.max(ticksY[0].map(function(d){ return d.getBBox().height; }));
divisor = Math.ceil(ticksMaxH / (config.cache.chartH / ticksY[0].length));
ticksY.filter(function(d, i){ return i%divisor !== 0; })
.style({display: 'none'});
ticksY.filter(function(d, i){ return i%divisor === 0; }).selectAll('line').classed('grid', true).attr({x1: config.cache.chartW})
return config;
}
// Render the geometries
function renderBarGeometry(config){
var barCountInGroup = config.cache.dataPixelSpace.length;
var barGroupW = config.cache.scaleX.rangeBand();
var barMargin = barGroupW/10;
var barW = (barGroupW - barMargin*2) / barCountInGroup;
var attr = {
width: barW - 1,
height: function(d){ return d.y; },
x: function(d, i, pI){ return i * barGroupW + pI * (barW) + barMargin; },
y: function(d, i){ return config.cache.chartH - d.y; }
};
var markGroupSelection = config.cache.root.select('g.geometry')
.selectAll('g.mark-group')
.data(config.cache.dataPixelSpace);
markGroupSelection.enter().append('g').attr({'class': 'mark-group'});
markGroupSelection.exit().remove();
var markSelection = markGroupSelection.selectAll('rect.mark')
.data(function(d){ return d; });
markSelection.enter().append('rect').attr({'class': 'mark'});
markSelection.attr(attr)
.each(function(d, i, pI){ d3.select(this).classed('color' + pI, true); });
markSelection.exit().remove();
return config;
}
function renderDotGeometry(config){
var attr = {
r: 3,
cx: function(d, i, pI){ return d.x; },
cy: function(d, i){ return config.cache.chartH - d.y; }
};
var markGroupSelection = config.cache.root.select('g.geometry')
.selectAll('g.mark-group')
.data(config.cache.dataPixelSpace);
markGroupSelection.enter().append('g').attr({'class': 'mark-group'});
markGroupSelection.exit().remove();
var markSelection = markGroupSelection.selectAll('circle.mark')
.data(function(d){ return d; });
markSelection.enter().append('circle').attr({'class': 'mark', transform: (config.cache.scaleX.rangeBand) ? 'translate('+[config.cache.scaleX.rangeBand()/2, 0]+')' : null});
markSelection.attr(attr)
.each(function(d, i, pI){ d3.select(this).classed('color' + pI, true); });
markSelection.exit().remove();
return config;
}
function renderLineGeometry(config){
var attr = {
r: 4,
cx: function(d, i){ var dotSpacing = config.cache.scaleX.rangeBand(); return i*dotSpacing + dotSpacing/2; },
cy: function(d, i){ return config.cache.chartH - d.y; }
};
var line = d3.svg.line()
.x(function(d){ return d.x })
.y(function(d){ return config.cache.chartH - d.y });
var lineSelection = config.cache.root.select('g.geometry')
.selectAll('path')
.data(config.cache.dataPixelSpace);
lineSelection.enter().append('path')
.attr({transform: 'translate('+[config.cache.scaleX.rangeBand()/2, 0]+')'})
.style({'pointer-events': 'none', fill: 'none'});
lineSelection.attr({
d: line
})
.each(function(d, i){ d3.select(this).classed('color' + i, true); });
lineSelection.exit().remove();
return config;
}
// Add interactors
function tooltip(config){
var tickSize = 10;
var padding = 2;
var tooltipSelection = config.cache.root.select('.hover').selectAll('g.tooltip')
.data([0]);
var tooltipEnter = tooltipSelection.enter().append('g').attr({'class': 'tooltip'})
.style({opacity: 0, 'pointer-events': 'none'});
tooltipEnter.append('path');
tooltipEnter.append('text').attr({dx: tickSize + padding, dy: 5});
config.cache.root.selectAll('.mark')
.on('mousemove', function(d, i, pI){
var color = window.getComputedStyle(this).fill;
var mousePos = d3.mouse(config.cache.root.select('.chart-panel').node());
tooltipSelection
.attr({transform: 'translate('+mousePos+')'})
.style({opacity: 1});
var textSelection = tooltipSelection.select('text')
.text(function(dB){
var dataXLength = config.cache.data[0].x.length;
var groupRank = +(i>dataXLength);
return config.cache.data[groupRank].x[i%dataXLength] +' '+ config.cache.data[groupRank].y[i%dataXLength];
});
var bbox = textSelection.node().getBBox();
var backGroundW = bbox.width + padding*2 + tickSize;
var backGroundH = bbox.height + padding*2;
tooltipSelection.select('path')
.attr({
d: 'M' + [[tickSize, -backGroundH/2],
[tickSize, -backGroundH/4], [0, 0], [tickSize, backGroundH/4],
[tickSize, backGroundH/2],
[backGroundW, backGroundH/2],
[backGroundW, -backGroundH/2]].join('L') + 'Z'
})
.style({fill: color, stroke: 'white'});
d3.select(this).classed('hovered', true);
})
.on('mouseout', function(){
tooltipSelection.style({opacity: 0});
d3.select(this).classed('hovered', false);
});
return config;
}
// Compose the visualization pipeline
/////////////////////////
var barChart = pipelineStart(init)
.then(prepareData)
.then(prepareCatNumScales)
.then(renderAxis)
.then(renderBarGeometry)
.pipelineEnd(tooltip);
var groupedBarChart = pipelineStart(init)
.then(prepareData)
.then(prepareCatNumScales)
.then(renderAxis)
.then(renderBarGeometry)
.pipelineEnd(tooltip);
var lineChart = pipelineStart(init)
.then(prepareData)
.then(prepareCatNumScales)
.then(renderAxis)
.then(renderLineGeometry)
.then(renderDotGeometry)
.pipelineEnd(tooltip);
var scatterplot = pipelineStart(init)
.then(prepareData)
.then(prepareNumNumScales)
.then(renderAxis)
.then(renderDotGeometry)
.pipelineEnd(tooltip);
// Usage
/////////////////////////
var dataCount = 20;
var generateCatNumData = function(){
return {
x: d3.range(dataCount).map(function (d, i) { return 'p'+i; }),
y: d3.range(dataCount).map(function (d, i) { return ~~(Math.random()*100); })
}
};
var generatedCatNumData = generateCatNumData();
var generateCatNumData2 = function(){
return {
x: generatedCatNumData.x,
y: d3.range(dataCount).map(function (d, i) { return ~~(Math.random()*100); })
}
};
var generateNumNumData = function(){
return {
x: d3.range(dataCount).map(function (d, i) { return ~~(Math.random()*100); }),
y: d3.range(dataCount).map(function (d, i) { return ~~(Math.random()*100); })
}
};
lineChart({
container: d3.select('#container').append('div').attr({class: 'chart1'}).node(),
data: [generatedCatNumData, generateCatNumData2(), generateCatNumData2(), generateCatNumData2()]
});
barChart({
container: d3.select('#container').append('div').attr({class: 'chart2'}).node(),
data: [generateCatNumData()]
});
groupedBarChart({
container: d3.select('#container').append('div').attr({class: 'chart3'}).node(),
data: [generatedCatNumData, generateCatNumData2(), generateCatNumData2()]
});
scatterplot({
container: d3.select('#container').append('div').attr({class: 'chart5'}).node(),
data: [generateNumNumData(), generateNumNumData(), generateNumNumData(), generateNumNumData()],
height: 300,
width: 300
});
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
<link rel="stylesheet" type="text/css" href="style_dark.css">
<style>
</style>
</head>
<body>
<div id="container"></div>
<script type="text/javascript" src="./chart.js"></script>
</body>
</html>
body{
background-color: rgb(30, 35, 40);
}
.tick line{
stroke: rgb(140, 145, 150);
}
.tick text{
fill: rgb(140, 145, 150);
font-size: 9px;
}
.domain{
stroke: rgb(70, 75, 80);
}
.mark{
fill: skyblue;
stroke: skyblue;
fill-opacity: 0.5;
}
.color0{
stroke: orange;
fill: orange;
fill-opacity: 0.5;
}
.color1{
stroke: lime;
fill: lime;
fill-opacity: 0.5;
}
.color2{
stroke: skyblue;
fill: skyblue;
fill-opacity: 0.5;
}
.color3{
stroke: violet;
fill: violet;
fill-opacity: 0.5;
}
.stripe0{
fill: rgb(70, 75, 80);
fill-opacity: 0.3;
}
.stripe1{
fill-opacity: 0;
}
.mark.hovered{
stroke-width: 3px;
}
.tick line.grid{
stroke: rgb(70, 75, 80);
}
.background .bg-rect{
fill: rgb(50, 55, 60);
stroke: black;
}
.tooltip text{
fill: rgb(70, 75, 80);
}
.tick line{
stroke: #aaa;
}
.tick text{
font-size: 9px;
}
.domain{
stroke: #aaa;
}
.mark{
fill: skyblue;
stroke: skyblue;
fill-opacity: 0.5;
}
path{
stroke: skyblue;
fill: none;
}
.color0{
stroke: orange;
fill: orange;
fill-opacity: 0.5;
}
.color1{
stroke: lime;
fill: lime;
fill-opacity: 0.5;
}
.color2{
stroke: skyblue;
fill: skyblue;
fill-opacity: 0.5;
}
.color3{
stroke: violet;
fill: violet;
fill-opacity: 0.5;
}
.stripe{
fill-opacity: 0;
}
.stripe0{
fill: rgb(250, 250, 250);
fill-opacity: 1;
}
.stripe1{
fill-opacity: 0;
}
.mark.hovered{
stroke-width: 3px;
}
.tick line.grid{
stroke: #eee;
}
.background .bg-rect{
fill: rgb(255, 255, 255);
stroke: rgb(240, 240, 240);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment