Development version of Piper.js showing how to build multiple charts sharing modules.
Last active
March 23, 2016 01:34
-
-
Save biovisualize/114170dfc9e64438f73c to your computer and use it in GitHub Desktop.
Piper.js multiple chart
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> | |
<head> | |
<meta charset="utf-8"> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script> | |
<script src="piper.js"></script> | |
<style> | |
.domain{ | |
fill: none; | |
} | |
.shape{ | |
fill: skyblue | |
} | |
line{ | |
stroke: #eee; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"></div> | |
<script> | |
var config = { | |
container: document.querySelector('.container'), | |
width: 400, | |
height: 300, | |
margin: {top: 40, right: 40, bottom: 60, left: 40}, | |
data: [1, 3, 4] | |
}; | |
var chart = piper.lineChart(config); | |
</script> | |
</body> |
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
var piper = {version: '0.1.0'}; | |
// Utilities | |
////////////////////////////////////////// | |
piper.utils = { | |
pipeline: function(){ | |
var fns = arguments; | |
var that = this; | |
return function(config) { | |
for (var i = 0; i < fns.length; i++) { | |
// console.log(i, 'before', config); | |
var cache = fns[i].call(this, config); | |
config = that.mergeAll(config, cache); | |
// console.log(i, 'after', config); | |
} | |
}; | |
}, | |
override: function(_objA, _objB){ for(var x in _objB){ if(x in _objA){ _objA[x] = _objB[x]; } } }, | |
merge: function(obj1, obj2) { | |
for (var p in obj2) { | |
if (obj2[p] && obj2[p].constructor == Object ) { | |
if (obj1[p]) { | |
this.merge(obj1[p], obj2[p]); | |
continue; | |
} | |
} | |
obj1[p] = obj2[p]; | |
} | |
}, | |
mergeAll: function(){ | |
var newObj = {}; | |
var objs = arguments; | |
for(var i = 0; i < objs.length; i++){ | |
this.merge(newObj, objs[i]); | |
} | |
return newObj; | |
} | |
}; | |
// Modules | |
////////////////////////////////////////// | |
piper.data = function(_config){ | |
var config = { | |
data: null | |
}; | |
piper.utils.override(config, _config); | |
var scaleType = config.data[0].timestamp ? 'time' : 'linear'; | |
var dataConverted = config.data.map(function(d, i){ | |
return { | |
x: d.timestamp ? d.timestamp : i, | |
y: d.value !== undefined ? d.value : d, | |
className: d.status | |
} | |
}); | |
return { | |
dataConverted: dataConverted, | |
scaleType: scaleType | |
}; | |
}; | |
piper.barChartAutoConfig = function(_config){ | |
return { | |
axisYPadding: 20 | |
}; | |
} | |
piper.scaleX = function(_config){ | |
var config = { | |
dataConverted: null, | |
margin: null, | |
width: null, | |
scaleType: null | |
}; | |
piper.utils.override(config, _config); | |
var chartWidth = config.width - config.margin.left - config.margin.right; | |
var dataX = config.dataConverted.map(function(d){ return d.x; }); | |
var scale = config.scaleType === 'time' ? d3.time.scale : d3.scale.linear; | |
var scaleX = scale() | |
.domain(d3.extent(dataX)) | |
.range([0, chartWidth]); | |
return { | |
scaleX: scaleX, | |
chartWidth: chartWidth | |
}; | |
}; | |
piper.scaleY = function(_config){ | |
var config = { | |
dataConverted: null, | |
margin: null, | |
height: null, | |
thresholdY: null | |
}; | |
piper.utils.override(config, _config); | |
var chartHeight = config.height - config.margin.top - config.margin.bottom; | |
var dataY = config.dataConverted.map(function(d){ return d.y; }); | |
var scaleMin = d3.min(dataY) - (d3.max(dataY) - d3.min(dataY)) / 2 | |
var max = d3.max(dataY); | |
if(typeof config.thresholdY === 'number' && config.thresholdY > max){ | |
max = config.thresholdY; | |
} | |
var scaleY = d3.scale.linear() | |
.domain([scaleMin, max]) | |
.range([chartHeight, 0]); | |
return { | |
scaleY: scaleY, | |
chartHeight: chartHeight | |
}; | |
}; | |
piper.axisX = function(_config){ | |
var config = { | |
scaleX: null, | |
axisXFormat: '%H:%M', | |
axisXTimeResolution: 'minutes', | |
axisXTimeSteps: 2 | |
}; | |
piper.utils.override(config, _config); | |
var timeResolution = d3.time[config.axisXTimeResolution] || d3.time.minutes; | |
var axisX = d3.svg.axis() | |
.scale(config.scaleX) | |
.ticks(timeResolution, config.axisXTimeSteps) | |
.tickFormat(d3.time.format(config.axisXFormat || '%H:%M')) | |
.orient('bottom'); | |
return { | |
axisX: axisX | |
}; | |
}; | |
piper.axisY = function(_config){ | |
var config = { | |
scaleY: null, | |
chartWidth: null, | |
margin: null, | |
axisYPadding: 0 | |
}; | |
piper.utils.override(config, _config); | |
var axisY = d3.svg.axis() | |
.scale(config.scaleY) | |
.orient('left') | |
.tickSize(-config.chartWidth - config.axisYPadding) | |
.ticks(6, 's') | |
.tickPadding(10); | |
return { | |
axisY: axisY | |
}; | |
}; | |
piper.panelComponent = function(_config){ | |
var config = { | |
container: null, | |
width: null, | |
height: null, | |
margin: null | |
}; | |
piper.utils.override(config, _config); | |
var root = d3.select(config.container) | |
.selectAll('svg') | |
.data([0]); | |
root.enter().append('svg') | |
.attr({ | |
'class': 'piper-chart' | |
}) | |
.append('g') | |
.attr({ | |
'class': 'panel' | |
}); | |
root.attr({ | |
width: config.width, | |
height: config.height | |
}); | |
root.exit().remove(); | |
var panel = root.select('g.panel') | |
.attr({ | |
transform: 'translate(' + config.margin.left + ',' + config.margin.top + ')' | |
}); | |
return { | |
root: root, | |
panel: panel | |
}; | |
}; | |
piper.axisComponentX = function(_config){ | |
var config = { | |
axisX: null, | |
chartHeight: null, | |
panel: null | |
}; | |
piper.utils.override(config, _config); | |
var axisX = config.panel.selectAll('g.axis.x') | |
.data([0]); | |
axisX.enter().append('g') | |
.attr({ | |
'class': 'x axis' | |
}); | |
axisX.attr({ | |
transform: 'translate(' + [0, config.chartHeight] + ')' | |
}) | |
.call(config.axisX); | |
axisX.exit().remove(); | |
return {}; | |
}; | |
piper.singleAxisComponentX = function(_config){ | |
var config = { | |
axisX: null, | |
panel: null | |
}; | |
piper.utils.override(config, _config); | |
var axisX = config.panel.selectAll('g.axis.x.single') | |
.data([0]); | |
axisX.enter().append('g') | |
.attr({ | |
'class': 'x axis single' | |
}); | |
axisX.call(config.axisX); | |
axisX.exit().remove(); | |
return {}; | |
}; | |
piper.axisComponentY = function(_config){ | |
var config = { | |
axisY: null, | |
panel: null, | |
axisYPadding: null | |
}; | |
piper.utils.override(config, _config); | |
var padding = config.axisYPadding || 0; | |
var axisY = config.panel.selectAll('g.axis.y') | |
.data([0]); | |
axisY.enter().append('g') | |
.attr({ | |
'class': 'y axis', | |
transform: 'translate(' + [-padding /2, 0] + ')' | |
}); | |
axisY.call(config.axisY); | |
axisY.exit().remove(); | |
return {}; | |
}; | |
piper.axisTitleComponentY = function(_config){ | |
var config = { | |
panel: null, | |
axisTitleY: null, | |
chartWidth: null, | |
chartHeight: null, | |
margin: null, | |
position: null | |
}; | |
piper.utils.override(config, _config); | |
var positionY = config.position === 'bottom' ? config.chartHeight + config.margin.top + config.margin.bottom / 4 : -10; | |
var positionX = config.position === 'bottom' ? function(){ return -10; } : function(){return config.chartWidth / 2 - this.getBBox().width / 2 + 10;}; | |
var axisTitleY = config.panel.selectAll('text.axis-title.y') | |
.data([0]); | |
axisTitleY.enter().append('text') | |
.attr({ | |
'class': 'y axis-title' | |
}); | |
axisTitleY.text(config.axisTitleY || '') | |
.attr({ | |
x: positionX, | |
y: positionY | |
}); | |
axisTitleY.exit().remove(); | |
return {}; | |
}; | |
piper.axisTitleBottomComponentY = function(_config){ | |
var config = { | |
panel: null, | |
axisTitleY: null, | |
chartHeight: null, | |
margin: null, | |
position: 'bottom' | |
}; | |
piper.utils.override(config, _config); | |
return piper.axisTitleComponentY(config); | |
}; | |
piper.areaShapes = function(_config){ | |
var config = { | |
panel: null, | |
dataConverted: null, | |
scaleX: null, | |
scaleY: null | |
}; | |
piper.utils.override(config, _config); | |
var dataY = config.dataConverted.map(function(d){ return d.y; }); | |
var area = d3.svg.area() | |
.defined(function(d) { return d.y != null; }) | |
.x(function(d){ return config.scaleX(d.x); }) | |
.y(function(d){ return config.scaleY(d.y); }) | |
.y0(config.scaleY.range()[0]); | |
var shapes = config.panel.selectAll('path.line') | |
.data([config.dataConverted]); | |
shapes.enter().append('path') | |
.attr({ | |
'class': 'line shape' | |
}); | |
shapes.attr({ | |
d: area | |
}); | |
shapes.exit().remove(); | |
return {}; | |
}; | |
piper.lineShapes = function(_config){ | |
var config = { | |
panel: null, | |
dataConverted: null, | |
scaleX: null, | |
scaleY: null | |
}; | |
piper.utils.override(config, _config); | |
var dataY = config.dataConverted.map(function(d){ return d.y; }); | |
var line = d3.svg.line() | |
.defined(function(d) { return d.y != null; }) | |
.x(function(d){ return config.scaleX(d.x); }) | |
.y(function(d){ return config.scaleY(d.y); }); | |
var shapes = config.panel.selectAll('path.line') | |
.data([config.dataConverted]); | |
shapes.enter().append('path') | |
.attr({ | |
'class': 'line shape' | |
}) | |
.style({fill: 'none'}); | |
shapes.attr({ | |
d: line | |
}); | |
shapes.exit().remove(); | |
return {}; | |
}; | |
piper.barShapes = function(_config){ | |
var config = { | |
panel: null, | |
dataConverted: null, | |
scaleX: null, | |
scaleY: null, | |
chartHeight: null | |
}; | |
piper.utils.override(config, _config); | |
var dataY = config.dataConverted.map(function(d){ return d.y; }); | |
var scaleXRange = config.scaleX.range(); | |
var width = scaleXRange[1] - scaleXRange[0]; | |
var barWidth = width / (config.dataConverted.length - 1) / 2; | |
var shapes = config.panel.selectAll('rect.bar') | |
.data(config.dataConverted); | |
shapes.enter().append('rect') | |
.attr({ | |
'class': function(d){ return 'bar shape ' + d.className; } | |
}); | |
shapes.attr({ | |
x: function(d){ return config.scaleX(d.x) - barWidth / 2; }, | |
y: function(d){ return config.scaleY(d.y); }, | |
width: function(d){ return barWidth; }, | |
height: function(d){ return config.chartHeight - config.scaleY(d.y); } | |
}); | |
shapes.exit().remove(); | |
return {}; | |
}; | |
piper.tresholdLine = function(_config){ | |
var config = { | |
panel: null, | |
dataConverted: null, | |
scaleX: null, | |
scaleY: null, | |
margin: null, | |
chartWidth: null, | |
thresholdY: null | |
}; | |
piper.utils.override(config, _config); | |
if(typeof config.thresholdY !== 'number'){ | |
return {}; | |
} | |
var scaledThresholdY = config.scaleY(config.thresholdY); | |
var path = 'M' + [[config.margin.left, scaledThresholdY], [config.chartWidth, scaledThresholdY]].join('L'); | |
var shapes = config.panel.selectAll('path.treshold') | |
.data([0]); | |
shapes.enter().append('path') | |
.attr({ | |
'class': 'treshold shape' | |
}) | |
.style({fill: 'none'}); | |
shapes.attr({ | |
d: path | |
}); | |
shapes.exit().remove(); | |
return {}; | |
}; | |
piper.verticalLine = function(_config){ | |
var config = { | |
panel: null, | |
dataConverted: null, | |
scaleX: null, | |
scaleY: null, | |
chartHeight: null, | |
margin: null, | |
verticalLineX: null, | |
verticalLineValue: null | |
}; | |
piper.utils.override(config, _config); | |
var scaledverticalLineX = config.scaleX(config.verticalLineX); | |
var path = 'M' + [[scaledverticalLineX, 0], [scaledverticalLineX, config.chartHeight]].join('L'); | |
var shapes = config.panel.selectAll('path.vertical-line') | |
.data([0]); | |
shapes.enter().append('path') | |
.attr({ | |
'class': 'vertical-line shape' | |
}); | |
shapes.attr({ | |
d: path | |
}); | |
shapes.exit().remove(); | |
var label = config.panel.selectAll('text.vertical-line-label') | |
.data([0]); | |
label.enter().append('text') | |
.attr({ | |
'class': 'vertical-line-label' | |
}); | |
label.attr({ | |
x: scaledverticalLineX + 2, | |
y: config.chartHeight + config.margin.top + config.margin.bottom / 4 | |
}) | |
.text(config.verticalLineValue); | |
shapes.exit().remove(); | |
return {}; | |
}; | |
piper.endCircle = function(_config){ | |
var config = { | |
panel: null, | |
dataConverted: null, | |
scaleX: null, | |
scaleY: null, | |
width: null | |
}; | |
piper.utils.override(config, _config); | |
var lastDataY = config.dataConverted[config.dataConverted.length-1]; | |
var shapes = config.panel.selectAll('circle.end-circle') | |
.data([lastDataY]); | |
shapes.enter().append('circle') | |
.attr({ | |
'class': 'end-circle shape' | |
}); | |
shapes.attr({ | |
cx: function(d){ return config.scaleX(d.x); }, | |
cy: function(d){ return config.scaleY(d.y); }, | |
r: 2 | |
}); | |
shapes.exit().remove(); | |
return {}; | |
}; | |
piper.axisXFormatter = function(_config){ | |
var config = { | |
panel: null, | |
dataConverted: null | |
}; | |
piper.utils.override(config, _config); | |
config.panel.select('g.axis.x.single') | |
.selectAll('.tick:first-child text') | |
.text(function(d){ | |
return d3.time.format('%a')(d); | |
}); | |
return {}; | |
}; | |
piper.eventCatchingLayer = function(_config){ | |
var config = { | |
panel: null, | |
chartWidth: null, | |
chartHeight: null, | |
}; | |
piper.utils.override(config, _config); | |
var eventPanelContainer = config.panel | |
.selectAll('g.event-panel-container') | |
.data([0]); | |
eventPanelContainer.enter().append('g') | |
.attr({ | |
'class': 'event-panel-container' | |
}) | |
.append('rect') | |
.attr({ | |
'class': 'event-panel' | |
}); | |
eventPanelContainer.exit().remove(); | |
var eventPanel = eventPanelContainer.select('.event-panel') | |
.attr({ | |
width: config.chartWidth, | |
height: config.chartHeight | |
}) | |
.style({ | |
visibility: 'hidden', | |
'pointer-events': 'all' | |
}); | |
return {eventPanel: eventPanel}; | |
} | |
piper.tooltipComponent = function(_config){ | |
var config = { | |
panel: null, | |
dataConverted: null, | |
scaleX: null, | |
scaleY: null, | |
eventPanel: null, | |
chartWidth: null, | |
chartHeight: null | |
}; | |
piper.utils.override(config, _config); | |
var newConfig = piper.eventCatchingLayer(config); | |
piper.utils.override(config, newConfig); | |
var dataConvertedX = config.dataConverted.map(function(d){ return d.x; }); | |
var deltaX = config.scaleX(dataConvertedX[1]) - config.scaleX(dataConvertedX[0]); | |
var tooltipGroup = config.panel | |
.selectAll('g.tooltip-container') | |
.data([0]); | |
tooltipGroup.enter().append('g') | |
.attr({ | |
'class': 'tooltip-container', | |
'pointer-events': 'none' | |
}) | |
.style({visibility: 'hidden'}) | |
.append('circle') | |
.attr({ | |
'class': 'tooltip-circle', | |
r: 3 | |
}); | |
tooltipGroup.exit().remove(); | |
var valueGroup = config.panel | |
.selectAll('g.value-container') | |
.data([0]); | |
valueGroup.enter().append('g') | |
.attr({ | |
'class': 'value-container', | |
'pointer-events': 'none' | |
}) | |
.style({visibility: 'hidden'}) | |
.append('text') | |
.attr({ | |
'class': 'value-label', | |
dy: -4 | |
}); | |
valueGroup.exit().remove(); | |
var lineGroup = config.panel | |
.selectAll('g.line-container') | |
.data([0]); | |
lineGroup.enter().append('g') | |
.attr({ | |
'class': 'line-container', | |
'pointer-events': 'none' | |
}) | |
.style({visibility: 'hidden'}) | |
.append('line') | |
.attr({ | |
'class': 'tooltip-line' | |
}); | |
lineGroup.exit().remove(); | |
var valueLabel = valueGroup.select('.value-label'); | |
var tooltipCircle = tooltipGroup.select('.tooltip-circle'); | |
var tooltipLine = lineGroup.select('.tooltip-line'); | |
config.eventPanel | |
.on('mouseenter', function(d){ | |
tooltipGroup.style({visibility: 'visible'}); | |
valueGroup.style({visibility: 'visible'}); | |
tooltipLine.style({visibility: 'visible'}); | |
}) | |
.on('mouseout', function(d){ | |
tooltipGroup.style({visibility: 'hidden'}); | |
valueGroup.style({visibility: 'hidden'}); | |
tooltipLine.style({visibility: 'hidden'}); | |
}) | |
.on('mousemove', function(d){ | |
var mouse = d3.mouse(this); | |
var dateAtCursor = config.scaleX.invert(mouse[0] - deltaX / 2); | |
var dataPointIndexAtCursor = d3.bisectLeft(dataConvertedX, dateAtCursor); | |
var dataPointAtCursor = config.dataConverted[dataPointIndexAtCursor]; | |
if(dataPointAtCursor){ | |
var date = dataPointAtCursor.x; | |
var value = dataPointAtCursor.y; | |
var x = config.scaleX(date); | |
var y = config.scaleY(value); | |
tooltipGroup.attr({transform: 'translate(' + [x, y] + ')'}); | |
valueGroup.attr({transform: 'translate(' + [0, y] + ')'}); | |
valueLabel.text(d3.round(value, 1)); | |
tooltipLine.attr({ | |
x1: 0, | |
y1: y, | |
x2: x, | |
y2: y | |
}); | |
} | |
}); | |
return {}; | |
}; | |
// Pipeline | |
////////////////////////////////////////// | |
piper.lineChart = piper.utils.pipeline( | |
piper.data, | |
piper.scaleX, | |
piper.scaleY, | |
piper.axisX, | |
piper.axisY, | |
piper.panelComponent, | |
piper.axisComponentX, | |
piper.axisTitleComponentY, | |
piper.areaShapes, | |
piper.axisComponentY, | |
piper.tooltipComponent | |
); | |
piper.sparkline = piper.utils.pipeline( | |
piper.data, | |
piper.scaleX, | |
piper.scaleY, | |
piper.panelComponent, | |
piper.lineShapes, | |
piper.tresholdLine, | |
piper.endCircle, | |
piper.tooltipComponent | |
); | |
piper.barChart = piper.utils.pipeline( | |
piper.data, | |
piper.barChartAutoConfig, | |
piper.scaleX, | |
piper.scaleY, | |
piper.axisX, | |
piper.axisY, | |
piper.panelComponent, | |
piper.barShapes, | |
piper.axisComponentY, | |
piper.axisComponentX, | |
piper.axisTitleBottomComponentY, | |
piper.verticalLine | |
); | |
piper.singleAxis = piper.utils.pipeline( | |
piper.data, | |
piper.scaleX, | |
piper.axisX, | |
piper.panelComponent, | |
piper.singleAxisComponentX, | |
piper.axisXFormatter | |
); | |
if (typeof module === "object" && module.exports) { | |
var d3 = require("d3"); | |
module.exports = piper; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment