| |
| nv.models.lineWithFocus = function() { |
| var margin = {top: 30, right: 20, bottom: 30, left: 60}, |
| margin2 = {top: 0, right: 20, bottom: 20, left: 60}, |
| width = 960, |
| height = 500, |
| height1 = 400, |
| height2 = 100, |
| color = d3.scale.category20().range(), |
| getX = function(d) { return d.x }, |
| getY = function(d) { return d.y }, |
| id = Math.floor(Math.random() * 10000); //Create semi-unique ID incase user doesn't select one |
| |
| var x = d3.scale.linear(), |
| y = d3.scale.linear(), |
| x2 = d3.scale.linear(), |
| y2 = d3.scale.linear(), |
| xAxis = nv.models.axis().scale(x).orient('bottom'), |
| yAxis = nv.models.axis().scale(y).orient('left'), |
| xAxis2 = nv.models.axis().scale(x2).orient('bottom'), |
| yAxis2 = nv.models.axis().scale(y2).orient('left'), |
| legend = nv.models.legend().height(30), |
| focus = nv.models.line().clipEdge(true), |
| context = nv.models.line().interactive(false), |
| dispatch = d3.dispatch('tooltipShow', 'tooltipHide'), |
| brush = d3.svg.brush() |
| .x(x2); |
| |
| |
| //var wrap, gEnter, g, focus, focusLines, contextWrap, focusWrap, contextLines; //brought all variables to this scope for use within brush function... is this a bad idea? |
| |
| //var seriesData; //Temporarily bringing this data to this scope.... may be bad idea (same with above).. may need to rethink brushing |
| |
| function chart(selection) { |
| selection.each(function(data) { |
| var seriesData = data.filter(function(d) { return !d.disabled }) |
| .map(function(d) { return d.values }), |
| availableWidth = width - margin.left - margin.right, |
| availableHeight1 = height1 - margin.top - margin.bottom, |
| availableHeight2 = height2 - margin2.top - margin2.bottom; |
| |
| x2 .domain(d3.extent(d3.merge(seriesData), getX )) |
| .range([0, availableWidth]); |
| y2 .domain(d3.extent(d3.merge(seriesData), getY )) |
| .range([availableHeight2, 0]); |
| |
| x .domain(brush.empty() ? x2.domain() : brush.extent()) |
| .range([0, availableWidth]); |
| y .domain(y2.domain()) |
| .range([availableHeight1, 0]); |
| |
| brush.on('brush', onBrush); |
| |
| focus |
| .width(availableWidth) |
| .height(availableHeight1) |
| .color(data.map(function(d,i) { |
| return d.color || color[i % 10]; |
| }).filter(function(d,i) { return !data[i].disabled })) |
| |
| context |
| .width(availableWidth) |
| .height(availableHeight2) |
| .color(data.map(function(d,i) { |
| return d.color || color[i % 10]; |
| }).filter(function(d,i) { return !data[i].disabled })) |
| |
| |
| updateFocus(); |
| |
| |
| var wrap = d3.select(this).selectAll('g.wrap').data([data]); |
| var gEnter = wrap.enter().append('g').attr('class', 'wrap nvd3 lineWithFocus').append('g'); |
| |
| gEnter.append('g').attr('class', 'focus'); |
| gEnter.append('g').attr('class', 'context'); |
| gEnter.append('g').attr('class', 'legendWrap'); |
| |
| |
| |
| var g = wrap.select('g') |
| //.attr('transform', 'translate(0,0)'); |
| |
| |
| |
| |
| // ********** LEGEND ********** |
| |
| legend.width(width/2 - margin.right); |
| |
| g.select('.legendWrap') |
| .datum(data) |
| .attr('transform', 'translate(' + (availableWidth / 2) + ',0)') |
| .call(legend); |
| |
| |
| //TODO: margins should be adjusted based on what components are used: axes, axis labels, legend |
| margin.top = legend.height(); |
| |
| |
| |
| |
| // ********** FOCUS ********** |
| |
| var focusWrap = g.select('.focus') |
| .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); |
| |
| gEnter.select('.focus').append('g').attr('class', 'x axis'); |
| gEnter.select('.focus').append('g').attr('class', 'y axis'); |
| gEnter.select('.focus').append('g').attr('class', 'focusLines'); |
| |
| |
| var focusLines = focusWrap.select('.focusLines') |
| .datum(data.filter(function(d) { return !d.disabled })) |
| |
| d3.transition(focusLines).call(focus); |
| |
| |
| xAxis |
| .domain(x.domain()) |
| .range(x.range()) |
| .ticks( width / 100 ) |
| .tickSize(-(availableHeight1), 0); |
| |
| focusWrap.select('.x.axis') |
| .attr('transform', 'translate(0,' + y.range()[0] + ')'); |
| d3.transition(g.select('.x.axis')) |
| .call(xAxis); |
| |
| yAxis |
| .domain(y.domain()) |
| .range(y.range()) |
| .ticks( height / 36 ) |
| .tickSize(-(availableWidth), 0); |
| |
| d3.transition(g.select('.y.axis')) |
| .call(yAxis); |
| |
| |
| |
| |
| // ********** CONTEXT ********** |
| |
| var contextWrap = g.select('.context') |
| .attr('transform', 'translate(' + margin2.left + ',' + height1 + ')'); |
| |
| gEnter.select('.context').append('g').attr('class', 'x2 axis'); |
| gEnter.select('.context').append('g').attr('class', 'y2 axis'); |
| gEnter.select('.context').append('g').attr('class', 'contextLines'); |
| gEnter.select('.context').append('g').attr('class', 'x brush') |
| .attr('class', 'x brush') |
| .call(brush) |
| .selectAll('rect') |
| .attr('y', -5) |
| .attr('height', height2 + 4); |
| |
| var contextLines = contextWrap.select('.contextLines') |
| .datum(data.filter(function(d) { return !d.disabled })) |
| |
| d3.transition(contextLines).call(context); |
| |
| |
| xAxis2 |
| .domain(x2.domain()) |
| .range(x2.range()) |
| .ticks( width / 100 ) |
| .tickSize(-(availableHeight2), 0); |
| |
| contextWrap.select('.x2.axis') |
| .attr('transform', 'translate(0,' + y2.range()[0] + ')'); |
| d3.transition(contextWrap.select('.x2.axis')) |
| .call(xAxis2); |
| |
| |
| yAxis2 |
| .domain(y2.domain()) |
| .range(y2.range()) |
| .ticks( availableHeight2 / 24 ) |
| .tickSize(-(availableWidth), 0); |
| |
| contextWrap.select('.y2.axis'); |
| |
| d3.transition(contextWrap.select('.y2.axis')) |
| .call(yAxis2); |
| |
| |
| |
| |
| |
| |
| // ********** EVENT LISTENERS ********** |
| |
| legend.dispatch.on('legendClick', function(d,i) { |
| d.disabled = !d.disabled; |
| |
| if (!data.filter(function(d) { return !d.disabled }).length) { |
| data.map(function(d) { |
| d.disabled = false; |
| wrap.selectAll('.series').classed('disabled', false); |
| return d; |
| }); |
| } |
| |
| selection.transition().call(chart); |
| }); |
| |
| /* |
| legend.dispatch.on('legendMouseover', function(d, i) { |
| d.hover = true; |
| selection.transition().call(chart) |
| }); |
| legend.dispatch.on('legendMouseout', function(d, i) { |
| d.hover = false; |
| selection.transition().call(chart) |
| }); |
| */ |
| |
| focus.dispatch.on('elementMouseover.tooltip', function(e) { |
| dispatch.tooltipShow({ |
| point: e.point, |
| series: e.series, |
| pos: [e.pos[0] + margin.left, e.pos[1] + margin.top], |
| seriesIndex: e.seriesIndex, |
| pointIndex: e.pointIndex |
| }); |
| }); |
| focus.dispatch.on('elementMouseout.tooltip', function(e) { |
| dispatch.tooltipHide(e); |
| }); |
| |
| |
| |
| |
| |
| function onBrush() { |
| updateFocus(); |
| |
| focusLines.call(focus) |
| wrap.select('.x.axis').call(xAxis); |
| wrap.select('.y.axis').call(yAxis); |
| } |
| |
| function updateFocus() { |
| var yDomain = brush.empty() ? y2.domain() : d3.extent(d3.merge(seriesData).filter(function(d) { |
| return getX(d) >= brush.extent()[0] && getX(d) <= brush.extent()[1]; |
| }), getY); //This doesn't account for the 1 point before and the 1 point after the domain. Would fix, but likely need to change entire methodology here |
| |
| if (typeof yDomain[0] == 'undefined') yDomain = y2.domain(); //incase the brush doesn't cover a single point |
| |
| |
| x.domain(brush.empty() ? x2.domain() : brush.extent()); |
| y.domain(yDomain); |
| |
| //TODO: Rethink this... performance is horrible, likely need to cut off focus data to within the range |
| // If I limit the data for focusLines would want to include 1 point before and after the extent, |
| // Need to figure out an optimized way to accomplish this. |
| // ***One concern is to try not to make the assumption that all lines are of the same length, and |
| // points with the same index have the same x value (while this is true in our test cases, may |
| // not always be) |
| |
| focus.xDomain(x.domain()); |
| focus.yDomain(y.domain()); |
| } |
| |
| |
| }); |
| |
| return chart; |
| } |
| |
| |
| |
| // ********** FUNCTIONS ********** |
| |
| |
| |
| |
| // ********** PUBLIC ACCESSORS ********** |
| |
| chart.dispatch = dispatch; |
| |
| chart.x = function(_) { |
| if (!arguments.length) return getX; |
| getX = _; |
| focus.x(_); |
| context.x(_); |
| return chart; |
| }; |
| |
| chart.y = function(_) { |
| if (!arguments.length) return getY; |
| getY = _; |
| focus.y(_); |
| context.y(_); |
| return chart; |
| }; |
| |
| chart.margin = function(_) { |
| if (!arguments.length) return margin; |
| margin = _; |
| return chart; |
| }; |
| |
| chart.width = function(_) { |
| if (!arguments.length) return width; |
| width = _; |
| return chart; |
| }; |
| |
| chart.height = function(_) { |
| if (!arguments.length) return height; |
| height = _; |
| height1 = _ - height2; |
| return chart; |
| }; |
| |
| chart.contextHeight = function(_) { |
| if (!arguments.length) return height2; |
| height2 = _; |
| height1 = height - _; |
| return chart; |
| }; |
| |
| chart.id = function(_) { |
| if (!arguments.length) return id; |
| id = _; |
| return chart; |
| }; |
| |
| |
| // Chart has multiple similar Axes, to prevent code duplication, probably need to link all axis functions manually like below |
| chart.xTickFormat = function(_) { |
| if (!arguments.length) return x.tickFormat(); |
| xAxis.tickFormat(_); |
| xAxis2.tickFormat(_); |
| return chart; |
| }; |
| |
| chart.yTickFormat = function(_) { |
| if (!arguments.length) return y.tickFormat(); |
| yAxis.tickFormat(_); |
| yAxis2.tickFormat(_); |
| return chart; |
| }; |
| |
| |
| |
| //TODO: allow for both focus and context axes to be linked |
| chart.xAxis = xAxis; |
| chart.yAxis = yAxis; |
| |
| |
| return chart; |
| } |