// 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 = '
' + '' + '' + '' + '' + '' + '' + '' + '
'; 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 });