Skip to content

Instantly share code, notes, and snippets.

@biovisualize
Last active March 23, 2016 01:34
Show Gist options
  • Save biovisualize/114170dfc9e64438f73c to your computer and use it in GitHub Desktop.
Save biovisualize/114170dfc9e64438f73c to your computer and use it in GitHub Desktop.
Piper.js multiple chart

Development version of Piper.js showing how to build multiple charts sharing modules.

<!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>
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