UNPKG

nvd3-fork

Version:

FORK! of NVD3, a reusable charting library written in d3.js

684 lines (595 loc) 29 kB
nv.models.multiChart = function() { "use strict"; //============================================================ // Public Variables with Default Settings //------------------------------------------------------------ var margin = {top: 30, right: 20, bottom: 50, left: 60}, marginTop = null, color = nv.utils.defaultColor(), width = null, height = null, showLegend = true, noData = null, yDomain1, yDomain2, getX = function(d) { return d.x }, getY = function(d) { return d.y}, interpolate = 'linear', useVoronoi = true, interactiveLayer = nv.interactiveGuideline(), useInteractiveGuideline = false, legendRightAxisHint = ' (right axis)', duration = 250 ; //============================================================ // Private Variables //------------------------------------------------------------ var x = d3.scale.linear(), yScale1 = d3.scale.linear(), yScale2 = d3.scale.linear(), lines1 = nv.models.line().yScale(yScale1).duration(duration), lines2 = nv.models.line().yScale(yScale2).duration(duration), scatters1 = nv.models.scatter().yScale(yScale1).duration(duration), scatters2 = nv.models.scatter().yScale(yScale2).duration(duration), bars1 = nv.models.multiBar().stacked(false).yScale(yScale1).duration(duration), bars2 = nv.models.multiBar().stacked(false).yScale(yScale2).duration(duration), stack1 = nv.models.stackedArea().yScale(yScale1).duration(duration), stack2 = nv.models.stackedArea().yScale(yScale2).duration(duration), xAxis = nv.models.axis().scale(x).orient('bottom').tickPadding(5).duration(duration), yAxis1 = nv.models.axis().scale(yScale1).orient('left').duration(duration), yAxis2 = nv.models.axis().scale(yScale2).orient('right').duration(duration), legend = nv.models.legend().height(30), tooltip = nv.models.tooltip(), dispatch = d3.dispatch(); var charts = [lines1, lines2, scatters1, scatters2, bars1, bars2, stack1, stack2]; function chart(selection) { selection.each(function(data) { var container = d3.select(this), that = this; nv.utils.initSVG(container); chart.update = function() { container.transition().call(chart); }; chart.container = this; var availableWidth = nv.utils.availableWidth(width, container, margin), availableHeight = nv.utils.availableHeight(height, container, margin); var dataLines1 = data.filter(function(d) {return d.type == 'line' && d.yAxis == 1}); var dataLines2 = data.filter(function(d) {return d.type == 'line' && d.yAxis == 2}); var dataScatters1 = data.filter(function(d) {return d.type == 'scatter' && d.yAxis == 1}); var dataScatters2 = data.filter(function(d) {return d.type == 'scatter' && d.yAxis == 2}); var dataBars1 = data.filter(function(d) {return d.type == 'bar' && d.yAxis == 1}); var dataBars2 = data.filter(function(d) {return d.type == 'bar' && d.yAxis == 2}); var dataStack1 = data.filter(function(d) {return d.type == 'area' && d.yAxis == 1}); var dataStack2 = data.filter(function(d) {return d.type == 'area' && d.yAxis == 2}); // Display noData message if there's nothing to show. if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) { nv.utils.noData(chart, container); return chart; } else { container.selectAll('.nv-noData').remove(); } var series1 = data.filter(function(d) {return !d.disabled && d.yAxis == 1}) .map(function(d) { return d.values.map(function(d,i) { return { x: getX(d), y: getY(d) } }) }); var series2 = data.filter(function(d) {return !d.disabled && d.yAxis == 2}) .map(function(d) { return d.values.map(function(d,i) { return { x: getX(d), y: getY(d) } }) }); x .domain(d3.extent(d3.merge(series1.concat(series2)), function(d) { return d.x })) .range([0, availableWidth]); var wrap = container.selectAll('g.wrap.multiChart').data([data]); var gEnter = wrap.enter().append('g').attr('class', 'wrap nvd3 multiChart').append('g'); gEnter.append('g').attr('class', 'nv-x nv-axis'); gEnter.append('g').attr('class', 'nv-y1 nv-axis'); gEnter.append('g').attr('class', 'nv-y2 nv-axis'); gEnter.append('g').attr('class', 'stack1Wrap'); gEnter.append('g').attr('class', 'stack2Wrap'); gEnter.append('g').attr('class', 'bars1Wrap'); gEnter.append('g').attr('class', 'bars2Wrap'); gEnter.append('g').attr('class', 'scatters1Wrap'); gEnter.append('g').attr('class', 'scatters2Wrap'); gEnter.append('g').attr('class', 'lines1Wrap'); gEnter.append('g').attr('class', 'lines2Wrap'); gEnter.append('g').attr('class', 'legendWrap'); gEnter.append('g').attr('class', 'nv-interactive'); var g = wrap.select('g'); var color_array = data.map(function(d,i) { return data[i].color || color(d, i); }); // Legend if (!showLegend) { g.select('.legendWrap').selectAll('*').remove(); } else { var legendWidth = legend.align() ? availableWidth / 2 : availableWidth; var legendXPosition = legend.align() ? legendWidth : 0; legend.width(legendWidth); legend.color(color_array); g.select('.legendWrap') .datum(data.map(function(series) { series.originalKey = series.originalKey === undefined ? series.key : series.originalKey; series.key = series.originalKey + (series.yAxis == 1 ? '' : legendRightAxisHint); return series; })) .call(legend); if (!marginTop && legend.height() !== margin.top) { margin.top = legend.height(); availableHeight = nv.utils.availableHeight(height, container, margin); } g.select('.legendWrap') .attr('transform', 'translate(' + legendXPosition + ',' + (-margin.top) +')'); } lines1 .width(availableWidth) .height(availableHeight) .interpolate(interpolate) .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 1 && data[i].type == 'line'})); lines2 .width(availableWidth) .height(availableHeight) .interpolate(interpolate) .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 2 && data[i].type == 'line'})); scatters1 .width(availableWidth) .height(availableHeight) .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 1 && data[i].type == 'scatter'})); scatters2 .width(availableWidth) .height(availableHeight) .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 2 && data[i].type == 'scatter'})); bars1 .width(availableWidth) .height(availableHeight) .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 1 && data[i].type == 'bar'})); bars2 .width(availableWidth) .height(availableHeight) .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 2 && data[i].type == 'bar'})); stack1 .width(availableWidth) .height(availableHeight) .interpolate(interpolate) .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 1 && data[i].type == 'area'})); stack2 .width(availableWidth) .height(availableHeight) .interpolate(interpolate) .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 2 && data[i].type == 'area'})); g.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); var lines1Wrap = g.select('.lines1Wrap') .datum(dataLines1.filter(function(d){return !d.disabled})); var scatters1Wrap = g.select('.scatters1Wrap') .datum(dataScatters1.filter(function(d){return !d.disabled})); var bars1Wrap = g.select('.bars1Wrap') .datum(dataBars1.filter(function(d){return !d.disabled})); var stack1Wrap = g.select('.stack1Wrap') .datum(dataStack1.filter(function(d){return !d.disabled})); var lines2Wrap = g.select('.lines2Wrap') .datum(dataLines2.filter(function(d){return !d.disabled})); var scatters2Wrap = g.select('.scatters2Wrap') .datum(dataScatters2.filter(function(d){return !d.disabled})); var bars2Wrap = g.select('.bars2Wrap') .datum(dataBars2.filter(function(d){return !d.disabled})); var stack2Wrap = g.select('.stack2Wrap') .datum(dataStack2.filter(function(d){return !d.disabled})); var extraValue1BarStacked = []; if (bars1.stacked() && dataBars1.length) { var extraValue1BarStacked = dataBars1.filter(function(d){return !d.disabled}).map(function(a){return a.values}); if (extraValue1BarStacked.length > 0) extraValue1BarStacked = extraValue1BarStacked.reduce(function(a,b){ return a.map(function(aVal,i){return {x: aVal.x, y: aVal.y + b[i].y}}) }); } if (dataBars1.length) { extraValue1BarStacked.push({x:0, y:0}); } var extraValue2BarStacked = []; if (bars2.stacked() && dataBars2.length) { var extraValue2BarStacked = dataBars2.filter(function(d){return !d.disabled}).map(function(a){return a.values}); if (extraValue2BarStacked.length > 0) extraValue2BarStacked = extraValue2BarStacked.reduce(function(a,b){ return a.map(function(aVal,i){return {x: aVal.x, y: aVal.y + b[i].y}}) }); } if (dataBars2.length) { extraValue2BarStacked.push({x:0, y:0}); } function getStackedAreaYs(series) { return d3.transpose(series).map(function(x) { return x.map(function(g) { return g.y; }); }).map(function(x) {return d3.sum(x);}) } yScale1 .domain(yDomain1 || d3.extent(d3.merge(series1).concat(extraValue1BarStacked), function(d) { return d.y } )) .range([0, availableHeight]); yScale2 .domain(yDomain2 || d3.extent(d3.merge(series2).concat(extraValue2BarStacked), function(d) { return d.y } )) .range([0, availableHeight]); lines1.yDomain(yScale1.domain()); scatters1.yDomain(yScale1.domain()); if(bars1.stacked()) { var yStackScale1 = yScale1.domain([0, d3.max(getStackedAreaYs(series1))]).range([0, availableHeight]); bars1.yDomain(yStackScale1.domain()) } else { bars1.yDomain(yScale1.domain()); } stack1.yDomain(yScale1.domain()); lines2.yDomain(yScale2.domain()); scatters2.yDomain(yScale2.domain()); if(bars2.stacked()) { var yStackScale2 = yScale2.domain([0, d3.max(getStackedAreaYs(series2))]).range([0, availableHeight]); bars2.yDomain(yStackScale2.domain()) } else { bars2.yDomain(yScale2.domain()); } stack2.yDomain(yScale2.domain()); if(dataStack1.length){d3.transition(stack1Wrap).call(stack1);} if(dataStack2.length){d3.transition(stack2Wrap).call(stack2);} if(dataBars1.length){d3.transition(bars1Wrap).call(bars1);} if(dataBars2.length){d3.transition(bars2Wrap).call(bars2);} if(dataLines1.length){d3.transition(lines1Wrap).call(lines1);} if(dataLines2.length){d3.transition(lines2Wrap).call(lines2);} if(dataScatters1.length){d3.transition(scatters1Wrap).call(scatters1);} if(dataScatters2.length){d3.transition(scatters2Wrap).call(scatters2);} xAxis ._ticks( nv.utils.calcTicksX(availableWidth/100, data) ) .tickSize(-availableHeight, 0); g.select('.nv-x.nv-axis') .attr('transform', 'translate(0,' + availableHeight + ')'); d3.transition(g.select('.nv-x.nv-axis')) .call(xAxis); yAxis1 ._ticks( nv.utils.calcTicksY(availableHeight/36, data) ) .tickSize( -availableWidth, 0); d3.transition(g.select('.nv-y1.nv-axis')) .call(yAxis1); yAxis2 ._ticks( nv.utils.calcTicksY(availableHeight/36, data) ) .tickSize( -availableWidth, 0); d3.transition(g.select('.nv-y2.nv-axis')) .call(yAxis2); g.select('.nv-y1.nv-axis') .classed('nv-disabled', series1.length ? false : true) .attr('transform', 'translate(' + x.range()[0] + ',0)'); g.select('.nv-y2.nv-axis') .classed('nv-disabled', series2.length ? false : true) .attr('transform', 'translate(' + x.range()[1] + ',0)'); legend.dispatch.on('stateChange', function(newState) { chart.update(); }); if(useInteractiveGuideline){ interactiveLayer .width(availableWidth) .height(availableHeight) .margin({left:margin.left, top:margin.top}) .svgContainer(container) .xScale(x); wrap.select(".nv-interactive").call(interactiveLayer); } //============================================================ // Event Handling/Dispatching //------------------------------------------------------------ function mouseover_line(evt) { var yaxis = evt.series.yAxis === 2 ? yAxis2 : yAxis1; evt.value = evt.point.x; evt.series = { value: evt.point.y, color: evt.point.color, key: evt.series.key }; tooltip .duration(0) .headerFormatter(function(d, i) { return xAxis.tickFormat()(d, i); }) .valueFormatter(function(d, i) { return yaxis.tickFormat()(d, i); }) .data(evt) .hidden(false); } function mouseover_scatter(evt) { var yaxis = evt.series.yAxis === 2 ? yAxis2 : yAxis1; evt.value = evt.point.x; evt.series = { value: evt.point.y, color: evt.point.color, key: evt.series.key }; tooltip .duration(100) .headerFormatter(function(d, i) { return xAxis.tickFormat()(d, i); }) .valueFormatter(function(d, i) { return yaxis.tickFormat()(d, i); }) .data(evt) .hidden(false); } function mouseover_stack(evt) { var yaxis = evt.series.yAxis === 2 ? yAxis2 : yAxis1; evt.point['x'] = stack1.x()(evt.point); evt.point['y'] = stack1.y()(evt.point); tooltip .duration(0) .headerFormatter(function(d, i) { return xAxis.tickFormat()(d, i); }) .valueFormatter(function(d, i) { return yaxis.tickFormat()(d, i); }) .data(evt) .hidden(false); } function mouseover_bar(evt) { var yaxis = evt.series.yAxis === 2 ? yAxis2 : yAxis1; evt.value = bars1.x()(evt.data); evt['series'] = { value: bars1.y()(evt.data), color: evt.color, key: evt.data.key }; tooltip .duration(0) .headerFormatter(function(d, i) { return xAxis.tickFormat()(d, i); }) .valueFormatter(function(d, i) { return yaxis.tickFormat()(d, i); }) .data(evt) .hidden(false); } function clearHighlights() { for(var i=0, il=charts.length; i < il; i++){ var chart = charts[i]; try { chart.clearHighlights(); } catch(e){} } } function highlightPoint(series, pointIndex, b, pointYValue) { var chartMap = { 'line': { 'yAxis1': { chart: lines1, data: dataLines1 }, 'yAxis2': { chart: lines2, data: dataLines2 } }, 'scatter': { 'yAxis1': { chart: scatters1, data: dataScatters1 }, 'yAxis2': { chart: scatters2, data: dataScatters2 } }, 'bar': { 'yAxis1': { chart: bars1, data: dataBars1 }, 'yAxis2': { chart: bars2, data: dataBars2 } }, 'area': { 'yAxis1': { chart: stack1, data: dataStack1 }, 'yAxis2': { chart: stack2, data: dataStack2 } } }; var relevantChart = chartMap[series.type]['yAxis' + series.yAxis].chart; var relevantDatasets = chartMap[series.type]['yAxis' + series.yAxis].data; var seriesIndex = relevantDatasets.reduce(function (seriesIndex, dataSet, i) { return dataSet.key === series.key ? i : seriesIndex; }, 0); try { relevantChart.highlightPoint(seriesIndex, pointIndex, b, pointYValue); } catch(e){} } if(useInteractiveGuideline){ interactiveLayer.dispatch.on('elementMousemove', function(e) { clearHighlights(); var singlePoint, pointIndex, pointXLocation, allData = []; data .filter(function(series, i) { series.seriesIndex = i; return !series.disabled; }) .forEach(function(series, i) { var extent = x.domain(); var currentValues = series.values.filter(function(d,i) { return chart.x()(d,i) >= extent[0] && chart.x()(d,i) <= extent[1]; }); pointIndex = nv.interactiveBisect(currentValues, e.pointXValue, chart.x()); var point = currentValues[pointIndex]; var pointYValue = chart.y()(point, pointIndex); if (pointYValue !== null && !isNaN(pointYValue) && !series.noHighlightSeries) { highlightPoint(series, pointIndex, true); } if (point === undefined) return; if (singlePoint === undefined) singlePoint = point; if (pointXLocation === undefined) pointXLocation = x(chart.x()(point,pointIndex)); allData.push({ key: series.key, value: pointYValue, color: color(series,series.seriesIndex), data: point, yAxis: series.yAxis == 2 ? yAxis2 : yAxis1 }); }); var defaultValueFormatter = function(d,i) { var yAxis = allData[i].yAxis; return d == null ? "N/A" : yAxis.tickFormat()(d); }; interactiveLayer.tooltip .headerFormatter(function(d, i) { return xAxis.tickFormat()(d, i); }) .valueFormatter(interactiveLayer.tooltip.valueFormatter() || defaultValueFormatter) .data({ value: chart.x()( singlePoint,pointIndex ), index: pointIndex, series: allData })(); interactiveLayer.renderGuideLine(pointXLocation); }); interactiveLayer.dispatch.on("elementMouseout",function(e) { clearHighlights(); }); } else { lines1.dispatch.on('elementMouseover.tooltip', mouseover_line); lines2.dispatch.on('elementMouseover.tooltip', mouseover_line); lines1.dispatch.on('elementMouseout.tooltip', function(evt) { tooltip.hidden(true) }); lines2.dispatch.on('elementMouseout.tooltip', function(evt) { tooltip.hidden(true) }); scatters1.dispatch.on('elementMouseover.tooltip', mouseover_scatter); scatters2.dispatch.on('elementMouseover.tooltip', mouseover_scatter); scatters1.dispatch.on('elementMouseout.tooltip', function(evt) { tooltip.hidden(true) }); scatters2.dispatch.on('elementMouseout.tooltip', function(evt) { tooltip.hidden(true) }); stack1.dispatch.on('elementMouseover.tooltip', mouseover_stack); stack2.dispatch.on('elementMouseover.tooltip', mouseover_stack); stack1.dispatch.on('elementMouseout.tooltip', function(evt) { tooltip.hidden(true) }); stack2.dispatch.on('elementMouseout.tooltip', function(evt) { tooltip.hidden(true) }); bars1.dispatch.on('elementMouseover.tooltip', mouseover_bar); bars2.dispatch.on('elementMouseover.tooltip', mouseover_bar); bars1.dispatch.on('elementMouseout.tooltip', function(evt) { tooltip.hidden(true); }); bars2.dispatch.on('elementMouseout.tooltip', function(evt) { tooltip.hidden(true); }); bars1.dispatch.on('elementMousemove.tooltip', function(evt) { tooltip(); }); bars2.dispatch.on('elementMousemove.tooltip', function(evt) { tooltip(); }); } }); return chart; } //============================================================ // Global getters and setters //------------------------------------------------------------ chart.dispatch = dispatch; chart.legend = legend; chart.lines1 = lines1; chart.lines2 = lines2; chart.scatters1 = scatters1; chart.scatters2 = scatters2; chart.bars1 = bars1; chart.bars2 = bars2; chart.stack1 = stack1; chart.stack2 = stack2; chart.xAxis = xAxis; chart.yAxis1 = yAxis1; chart.yAxis2 = yAxis2; chart.tooltip = tooltip; chart.interactiveLayer = interactiveLayer; chart.options = nv.utils.optionsFunc.bind(chart); chart._options = Object.create({}, { // simple options, just get/set the necessary values width: {get: function(){return width;}, set: function(_){width=_;}}, height: {get: function(){return height;}, set: function(_){height=_;}}, showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}}, xScale: {get: function(){return x;}, set: function(_){ x = _; xAxis.scale(x); }}, yDomain1: {get: function(){return yDomain1;}, set: function(_){yDomain1=_;}}, yDomain2: {get: function(){return yDomain2;}, set: function(_){yDomain2=_;}}, noData: {get: function(){return noData;}, set: function(_){noData=_;}}, interpolate: {get: function(){return interpolate;}, set: function(_){interpolate=_;}}, legendRightAxisHint: {get: function(){return legendRightAxisHint;}, set: function(_){legendRightAxisHint=_;}}, // options that require extra logic in the setter margin: {get: function(){return margin;}, set: function(_){ if (_.top !== undefined) { margin.top = _.top; marginTop = _.top; } margin.right = _.right !== undefined ? _.right : margin.right; margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; margin.left = _.left !== undefined ? _.left : margin.left; }}, color: {get: function(){return color;}, set: function(_){ color = nv.utils.getColor(_); }}, x: {get: function(){return getX;}, set: function(_){ getX = _; lines1.x(_); lines2.x(_); scatters1.x(_); scatters2.x(_); bars1.x(_); bars2.x(_); stack1.x(_); stack2.x(_); }}, y: {get: function(){return getY;}, set: function(_){ getY = _; lines1.y(_); lines2.y(_); scatters1.y(_); scatters2.y(_); stack1.y(_); stack2.y(_); bars1.y(_); bars2.y(_); }}, useVoronoi: {get: function(){return useVoronoi;}, set: function(_){ useVoronoi=_; lines1.useVoronoi(_); lines2.useVoronoi(_); stack1.useVoronoi(_); stack2.useVoronoi(_); }}, useInteractiveGuideline: {get: function(){return useInteractiveGuideline;}, set: function(_){ useInteractiveGuideline = _; if (useInteractiveGuideline) { lines1.interactive(false); lines1.useVoronoi(false); lines2.interactive(false); lines2.useVoronoi(false); stack1.interactive(false); stack1.useVoronoi(false); stack2.interactive(false); stack2.useVoronoi(false); scatters1.interactive(false); scatters2.interactive(false); } }}, duration: {get: function(){return duration;}, set: function(_) { duration = _; [lines1, lines2, stack1, stack2, scatters1, scatters2, xAxis, yAxis1, yAxis2].forEach(function(model){ model.duration(duration); }); }} }); nv.utils.initOptions(chart); return chart; };