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