Created
May 8, 2014 04:41
-
-
Save biovisualize/eaa17e4f9b72c3cc6145 to your computer and use it in GitHub Desktop.
visualization pipeline library
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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