| |
| nv.models.scatter = function() { |
| "use strict"; |
| //============================================================ |
| // Public Variables with Default Settings |
| //------------------------------------------------------------ |
| |
| var margin = {top: 0, right: 0, bottom: 0, left: 0} |
| , width = 960 |
| , height = 500 |
| , color = nv.utils.defaultColor() // chooses color |
| , id = Math.floor(Math.random() * 100000) //Create semi-unique ID incase user doesn't select one |
| , x = d3.scale.linear() |
| , y = d3.scale.linear() |
| , z = d3.scale.linear() //linear because d3.svg.shape.size is treated as area |
| , getX = function(d) { return d.x } // accessor to get the x value |
| , getY = function(d) { return d.y } // accessor to get the y value |
| , getSize = function(d) { return d.size || 1} // accessor to get the point size |
| , getShape = function(d) { return d.shape || 'circle' } // accessor to get point shape |
| , onlyCircles = true // Set to false to use shapes |
| , forceX = [] // List of numbers to Force into the X scale (ie. 0, or a max / min, etc.) |
| , forceY = [] // List of numbers to Force into the Y scale |
| , forceSize = [] // List of numbers to Force into the Size scale |
| , interactive = true // If true, plots a voronoi overlay for advanced point intersection |
| , pointKey = null |
| , pointActive = function(d) { return !d.notActive } // any points that return false will be filtered out |
| , padData = false // If true, adds half a data points width to front and back, for lining up a line chart with a bar chart |
| , padDataOuter = .1 //outerPadding to imitate ordinal scale outer padding |
| , clipEdge = false // if true, masks points within x and y scale |
| , clipVoronoi = true // if true, masks each point with a circle... can turn off to slightly increase performance |
| , clipRadius = function() { return 25 } // function to get the radius for voronoi point clips |
| , xDomain = null // Override x domain (skips the calculation from data) |
| , yDomain = null // Override y domain |
| , xRange = null // Override x range |
| , yRange = null // Override y range |
| , sizeDomain = null // Override point size domain |
| , sizeRange = null |
| , singlePoint = false |
| , dispatch = d3.dispatch('elementClick', 'elementMouseover', 'elementMouseout') |
| , useVoronoi = true |
| ; |
| |
| //============================================================ |
| |
| |
| //============================================================ |
| // Private Variables |
| //------------------------------------------------------------ |
| |
| var x0, y0, z0 // used to store previous scales |
| , timeoutID |
| , needsUpdate = false // Flag for when the points are visually updating, but the interactive layer is behind, to disable tooltips |
| ; |
| |
| //============================================================ |
| |
| |
| function chart(selection) { |
| selection.each(function(data) { |
| var availableWidth = width - margin.left - margin.right, |
| availableHeight = height - margin.top - margin.bottom, |
| container = d3.select(this); |
| |
| //add series index to each data point for reference |
| data.forEach(function(series, i) { |
| series.values.forEach(function(point) { |
| point.series = i; |
| }); |
| }); |
| |
| //------------------------------------------------------------ |
| // Setup Scales |
| |
| // remap and flatten the data for use in calculating the scales' domains |
| var seriesData = (xDomain && yDomain && sizeDomain) ? [] : // if we know xDomain and yDomain and sizeDomain, no need to calculate.... if Size is constant remember to set sizeDomain to speed up performance |
| d3.merge( |
| data.map(function(d) { |
| return d.values.map(function(d,i) { |
| return { x: getX(d,i), y: getY(d,i), size: getSize(d,i) } |
| }) |
| }) |
| ); |
| |
| x .domain(xDomain || d3.extent(seriesData.map(function(d) { return d.x; }).concat(forceX))) |
| |
| if (padData && data[0]) |
| x.range(xRange || [(availableWidth * padDataOuter + availableWidth) / (2 *data[0].values.length), availableWidth - availableWidth * (1 + padDataOuter) / (2 * data[0].values.length) ]); |
| //x.range([availableWidth * .5 / data[0].values.length, availableWidth * (data[0].values.length - .5) / data[0].values.length ]); |
| else |
| x.range(xRange || [0, availableWidth]); |
| |
| y .domain(yDomain || d3.extent(seriesData.map(function(d) { return d.y }).concat(forceY))) |
| .range(yRange || [availableHeight, 0]); |
| |
| z .domain(sizeDomain || d3.extent(seriesData.map(function(d) { return d.size }).concat(forceSize))) |
| .range(sizeRange || [16, 256]); |
| |
| // If scale's domain don't have a range, slightly adjust to make one... so a chart can show a single data point |
| if (x.domain()[0] === x.domain()[1] || y.domain()[0] === y.domain()[1]) singlePoint = true; |
| if (x.domain()[0] === x.domain()[1]) |
| x.domain()[0] ? |
| x.domain([x.domain()[0] - x.domain()[0] * 0.01, x.domain()[1] + x.domain()[1] * 0.01]) |
| : x.domain([-1,1]); |
| |
| if (y.domain()[0] === y.domain()[1]) |
| y.domain()[0] ? |
| y.domain([y.domain()[0] - y.domain()[0] * 0.01, y.domain()[1] + y.domain()[1] * 0.01]) |
| : y.domain([-1,1]); |
| |
| if ( isNaN(x.domain()[0])) { |
| x.domain([-1,1]); |
| } |
| |
| if ( isNaN(y.domain()[0])) { |
| y.domain([-1,1]); |
| } |
| |
| |
| x0 = x0 || x; |
| y0 = y0 || y; |
| z0 = z0 || z; |
| |
| //------------------------------------------------------------ |
| |
| |
| //------------------------------------------------------------ |
| // Setup containers and skeleton of chart |
| |
| var wrap = container.selectAll('g.nv-wrap.nv-scatter').data([data]); |
| var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-scatter nv-chart-' + id + (singlePoint ? ' nv-single-point' : '')); |
| var defsEnter = wrapEnter.append('defs'); |
| var gEnter = wrapEnter.append('g'); |
| var g = wrap.select('g'); |
| |
| gEnter.append('g').attr('class', 'nv-groups'); |
| gEnter.append('g').attr('class', 'nv-point-paths'); |
| |
| wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); |
| |
| //------------------------------------------------------------ |
| |
| |
| defsEnter.append('clipPath') |
| .attr('id', 'nv-edge-clip-' + id) |
| .append('rect'); |
| |
| wrap.select('#nv-edge-clip-' + id + ' rect') |
| .attr('width', availableWidth) |
| .attr('height', availableHeight); |
| |
| g .attr('clip-path', clipEdge ? 'url(#nv-edge-clip-' + id + ')' : ''); |
| |
| |
| function updateInteractiveLayer() { |
| |
| if (!interactive) return false; |
| |
| var eventElements; |
| |
| var vertices = d3.merge(data.map(function(group, groupIndex) { |
| return group.values |
| .map(function(point, pointIndex) { |
| // *Adding noise to make duplicates very unlikely |
| // *Injecting series and point index for reference |
| /* *Adding a 'jitter' to the points, because there's an issue in d3.geom.voronoi. |
| */ |
| var pX = getX(point,pointIndex); |
| var pY = getY(point,pointIndex); |
| |
| return [x(pX)+ Math.random() * 1e-7, |
| y(pY)+ Math.random() * 1e-7, |
| groupIndex, |
| pointIndex, point]; //temp hack to add noise untill I think of a better way so there are no duplicates |
| }) |
| .filter(function(pointArray, pointIndex) { |
| return pointActive(pointArray[4], pointIndex); // Issue #237.. move filter to after map, so pointIndex is correct! |
| }) |
| }) |
| ); |
| |
| |
| |
| //inject series and point index for reference into voronoi |
| if (useVoronoi === true) { |
| |
| if (clipVoronoi) { |
| var pointClipsEnter = wrap.select('defs').selectAll('.nv-point-clips') |
| .data([id]) |
| .enter(); |
| |
| pointClipsEnter.append('clipPath') |
| .attr('class', 'nv-point-clips') |
| .attr('id', 'nv-points-clip-' + id); |
| |
| var pointClips = wrap.select('#nv-points-clip-' + id).selectAll('circle') |
| .data(vertices); |
| pointClips.enter().append('circle') |
| .attr('r', clipRadius); |
| pointClips.exit().remove(); |
| pointClips |
| .attr('cx', function(d) { return d[0] }) |
| .attr('cy', function(d) { return d[1] }); |
| |
| wrap.select('.nv-point-paths') |
| .attr('clip-path', 'url(#nv-points-clip-' + id + ')'); |
| } |
| |
| |
| if(vertices.length) { |
| // Issue #283 - Adding 2 dummy points to the voronoi b/c voronoi requires min 3 points to work |
| vertices.push([x.range()[0] - 20, y.range()[0] - 20, null, null]); |
| vertices.push([x.range()[1] + 20, y.range()[1] + 20, null, null]); |
| vertices.push([x.range()[0] - 20, y.range()[0] + 20, null, null]); |
| vertices.push([x.range()[1] + 20, y.range()[1] - 20, null, null]); |
| } |
| |
| var bounds = d3.geom.polygon([ |
| [-10,-10], |
| [-10,height + 10], |
| [width + 10,height + 10], |
| [width + 10,-10] |
| ]); |
| |
| var voronoi = d3.geom.voronoi(vertices).map(function(d, i) { |
| return { |
| 'data': bounds.clip(d), |
| 'series': vertices[i][2], |
| 'point': vertices[i][3] |
| } |
| }); |
| |
| |
| var pointPaths = wrap.select('.nv-point-paths').selectAll('path') |
| .data(voronoi); |
| pointPaths.enter().append('path') |
| .attr('class', function(d,i) { return 'nv-path-'+i; }); |
| pointPaths.exit().remove(); |
| pointPaths |
| .attr('d', function(d) { |
| if (d.data.length === 0) |
| return 'M 0 0' |
| else |
| return 'M' + d.data.join('L') + 'Z'; |
| }); |
| |
| var mouseEventCallback = function(d,mDispatch) { |
| if (needsUpdate) return 0; |
| var series = data[d.series]; |
| if (typeof series === 'undefined') return; |
| |
| var point = series.values[d.point]; |
| |
| mDispatch({ |
| point: point, |
| series: series, |
| pos: [x(getX(point, d.point)) + margin.left, y(getY(point, d.point)) + margin.top], |
| seriesIndex: d.series, |
| pointIndex: d.point |
| }); |
| }; |
| |
| pointPaths |
| .on('click', function(d) { |
| mouseEventCallback(d, dispatch.elementClick); |
| }) |
| .on('mouseover', function(d) { |
| mouseEventCallback(d, dispatch.elementMouseover); |
| }) |
| .on('mouseout', function(d, i) { |
| mouseEventCallback(d, dispatch.elementMouseout); |
| }); |
| |
| |
| } else { |
| /* |
| // bring data in form needed for click handlers |
| var dataWithPoints = vertices.map(function(d, i) { |
| return { |
| 'data': d, |
| 'series': vertices[i][2], |
| 'point': vertices[i][3] |
| } |
| }); |
| */ |
| |
| // add event handlers to points instead voronoi paths |
| wrap.select('.nv-groups').selectAll('.nv-group') |
| .selectAll('.nv-point') |
| //.data(dataWithPoints) |
| //.style('pointer-events', 'auto') // recativate events, disabled by css |
| .on('click', function(d,i) { |
| //nv.log('test', d, i); |
| if (needsUpdate || !data[d.series]) return 0; //check if this is a dummy point |
| var series = data[d.series], |
| point = series.values[i]; |
| |
| dispatch.elementClick({ |
| point: point, |
| series: series, |
| pos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top], |
| seriesIndex: d.series, |
| pointIndex: i |
| }); |
| }) |
| .on('mouseover', function(d,i) { |
| if (needsUpdate || !data[d.series]) return 0; //check if this is a dummy point |
| var series = data[d.series], |
| point = series.values[i]; |
| |
| dispatch.elementMouseover({ |
| point: point, |
| series: series, |
| pos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top], |
| seriesIndex: d.series, |
| pointIndex: i |
| }); |
| }) |
| .on('mouseout', function(d,i) { |
| if (needsUpdate || !data[d.series]) return 0; //check if this is a dummy point |
| var series = data[d.series], |
| point = series.values[i]; |
| |
| dispatch.elementMouseout({ |
| point: point, |
| series: series, |
| seriesIndex: d.series, |
| pointIndex: i |
| }); |
| }); |
| } |
| |
| needsUpdate = false; |
| } |
| |
| needsUpdate = true; |
| |
| var groups = wrap.select('.nv-groups').selectAll('.nv-group') |
| .data(function(d) { return d }, function(d) { return d.key }); |
| groups.enter().append('g') |
| .style('stroke-opacity', 1e-6) |
| .style('fill-opacity', 1e-6); |
| groups.exit() |
| .remove(); |
| groups |
| .attr('class', function(d,i) { return 'nv-group nv-series-' + i }) |
| .classed('hover', function(d) { return d.hover }); |
| groups |
| .transition() |
| .style('fill', function(d,i) { return color(d, i) }) |
| .style('stroke', function(d,i) { return color(d, i) }) |
| .style('stroke-opacity', 1) |
| .style('fill-opacity', .5); |
| |
| |
| if (onlyCircles) { |
| |
| var points = groups.selectAll('circle.nv-point') |
| .data(function(d) { return d.values }, pointKey); |
| points.enter().append('circle') |
| .style('fill', function (d,i) { return d.color }) |
| .style('stroke', function (d,i) { return d.color }) |
| .attr('cx', function(d,i) { return nv.utils.NaNtoZero(x0(getX(d,i))) }) |
| .attr('cy', function(d,i) { return nv.utils.NaNtoZero(y0(getY(d,i))) }) |
| .attr('r', function(d,i) { return Math.sqrt(z(getSize(d,i))/Math.PI) }); |
| points.exit().remove(); |
| groups.exit().selectAll('path.nv-point').transition() |
| .attr('cx', function(d,i) { return nv.utils.NaNtoZero(x(getX(d,i))) }) |
| .attr('cy', function(d,i) { return nv.utils.NaNtoZero(y(getY(d,i))) }) |
| .remove(); |
| points.each(function(d,i) { |
| d3.select(this) |
| .classed('nv-point', true) |
| .classed('nv-point-' + i, true) |
| .classed('hover',false) |
| ; |
| }); |
| points.transition() |
| .attr('cx', function(d,i) { return nv.utils.NaNtoZero(x(getX(d,i))) }) |
| .attr('cy', function(d,i) { return nv.utils.NaNtoZero(y(getY(d,i))) }) |
| .attr('r', function(d,i) { return Math.sqrt(z(getSize(d,i))/Math.PI) }); |
| |
| } else { |
| |
| var points = groups.selectAll('path.nv-point') |
| .data(function(d) { return d.values }); |
| points.enter().append('path') |
| .style('fill', function (d,i) { return d.color }) |
| .style('stroke', function (d,i) { return d.color }) |
| .attr('transform', function(d,i) { |
| return 'translate(' + x0(getX(d,i)) + ',' + y0(getY(d,i)) + ')' |
| }) |
| .attr('d', |
| d3.svg.symbol() |
| .type(getShape) |
| .size(function(d,i) { return z(getSize(d,i)) }) |
| ); |
| points.exit().remove(); |
| groups.exit().selectAll('path.nv-point') |
| .transition() |
| .attr('transform', function(d,i) { |
| return 'translate(' + x(getX(d,i)) + ',' + y(getY(d,i)) + ')' |
| }) |
| .remove(); |
| points.each(function(d,i) { |
| d3.select(this) |
| .classed('nv-point', true) |
| .classed('nv-point-' + i, true) |
| .classed('hover',false) |
| ; |
| }); |
| points.transition() |
| .attr('transform', function(d,i) { |
| //nv.log(d,i,getX(d,i), x(getX(d,i))); |
| return 'translate(' + x(getX(d,i)) + ',' + y(getY(d,i)) + ')' |
| }) |
| .attr('d', |
| d3.svg.symbol() |
| .type(getShape) |
| .size(function(d,i) { return z(getSize(d,i)) }) |
| ); |
| } |
| |
| |
| // Delay updating the invisible interactive layer for smoother animation |
| clearTimeout(timeoutID); // stop repeat calls to updateInteractiveLayer |
| timeoutID = setTimeout(updateInteractiveLayer, 300); |
| //updateInteractiveLayer(); |
| |
| //store old scales for use in transitions on update |
| x0 = x.copy(); |
| y0 = y.copy(); |
| z0 = z.copy(); |
| |
| }); |
| |
| return chart; |
| } |
| |
| |
| //============================================================ |
| // Event Handling/Dispatching (out of chart's scope) |
| //------------------------------------------------------------ |
| chart.clearHighlights = function() { |
| //Remove the 'hover' class from all highlighted points. |
| d3.selectAll(".nv-chart-" + id + " .nv-point.hover").classed("hover",false); |
| }; |
| |
| chart.highlightPoint = function(seriesIndex,pointIndex,isHoverOver) { |
| d3.select(".nv-chart-" + id + " .nv-series-" + seriesIndex + " .nv-point-" + pointIndex) |
| .classed("hover",isHoverOver); |
| }; |
| |
| |
| dispatch.on('elementMouseover.point', function(d) { |
| if (interactive) chart.highlightPoint(d.seriesIndex,d.pointIndex,true); |
| }); |
| |
| dispatch.on('elementMouseout.point', function(d) { |
| if (interactive) chart.highlightPoint(d.seriesIndex,d.pointIndex,false); |
| }); |
| |
| //============================================================ |
| |
| |
| //============================================================ |
| // Expose Public Variables |
| //------------------------------------------------------------ |
| |
| chart.dispatch = dispatch; |
| chart.options = nv.utils.optionsFunc.bind(chart); |
| |
| chart.x = function(_) { |
| if (!arguments.length) return getX; |
| getX = d3.functor(_); |
| return chart; |
| }; |
| |
| chart.y = function(_) { |
| if (!arguments.length) return getY; |
| getY = d3.functor(_); |
| return chart; |
| }; |
| |
| chart.size = function(_) { |
| if (!arguments.length) return getSize; |
| getSize = d3.functor(_); |
| return chart; |
| }; |
| |
| chart.margin = function(_) { |
| if (!arguments.length) return margin; |
| margin.top = typeof _.top != 'undefined' ? _.top : margin.top; |
| margin.right = typeof _.right != 'undefined' ? _.right : margin.right; |
| margin.bottom = typeof _.bottom != 'undefined' ? _.bottom : margin.bottom; |
| margin.left = typeof _.left != 'undefined' ? _.left : margin.left; |
| return chart; |
| }; |
| |
| chart.width = function(_) { |
| if (!arguments.length) return width; |
| width = _; |
| return chart; |
| }; |
| |
| chart.height = function(_) { |
| if (!arguments.length) return height; |
| height = _; |
| return chart; |
| }; |
| |
| chart.xScale = function(_) { |
| if (!arguments.length) return x; |
| x = _; |
| return chart; |
| }; |
| |
| chart.yScale = function(_) { |
| if (!arguments.length) return y; |
| y = _; |
| return chart; |
| }; |
| |
| chart.zScale = function(_) { |
| if (!arguments.length) return z; |
| z = _; |
| return chart; |
| }; |
| |
| chart.xDomain = function(_) { |
| if (!arguments.length) return xDomain; |
| xDomain = _; |
| return chart; |
| }; |
| |
| chart.yDomain = function(_) { |
| if (!arguments.length) return yDomain; |
| yDomain = _; |
| return chart; |
| }; |
| |
| chart.sizeDomain = function(_) { |
| if (!arguments.length) return sizeDomain; |
| sizeDomain = _; |
| return chart; |
| }; |
| |
| chart.xRange = function(_) { |
| if (!arguments.length) return xRange; |
| xRange = _; |
| return chart; |
| }; |
| |
| chart.yRange = function(_) { |
| if (!arguments.length) return yRange; |
| yRange = _; |
| return chart; |
| }; |
| |
| chart.sizeRange = function(_) { |
| if (!arguments.length) return sizeRange; |
| sizeRange = _; |
| return chart; |
| }; |
| |
| chart.forceX = function(_) { |
| if (!arguments.length) return forceX; |
| forceX = _; |
| return chart; |
| }; |
| |
| chart.forceY = function(_) { |
| if (!arguments.length) return forceY; |
| forceY = _; |
| return chart; |
| }; |
| |
| chart.forceSize = function(_) { |
| if (!arguments.length) return forceSize; |
| forceSize = _; |
| return chart; |
| }; |
| |
| chart.interactive = function(_) { |
| if (!arguments.length) return interactive; |
| interactive = _; |
| return chart; |
| }; |
| |
| chart.pointKey = function(_) { |
| if (!arguments.length) return pointKey; |
| pointKey = _; |
| return chart; |
| }; |
| |
| chart.pointActive = function(_) { |
| if (!arguments.length) return pointActive; |
| pointActive = _; |
| return chart; |
| }; |
| |
| chart.padData = function(_) { |
| if (!arguments.length) return padData; |
| padData = _; |
| return chart; |
| }; |
| |
| chart.padDataOuter = function(_) { |
| if (!arguments.length) return padDataOuter; |
| padDataOuter = _; |
| return chart; |
| }; |
| |
| chart.clipEdge = function(_) { |
| if (!arguments.length) return clipEdge; |
| clipEdge = _; |
| return chart; |
| }; |
| |
| chart.clipVoronoi= function(_) { |
| if (!arguments.length) return clipVoronoi; |
| clipVoronoi = _; |
| return chart; |
| }; |
| |
| chart.useVoronoi= function(_) { |
| if (!arguments.length) return useVoronoi; |
| useVoronoi = _; |
| if (useVoronoi === false) { |
| clipVoronoi = false; |
| } |
| return chart; |
| }; |
| |
| chart.clipRadius = function(_) { |
| if (!arguments.length) return clipRadius; |
| clipRadius = _; |
| return chart; |
| }; |
| |
| chart.color = function(_) { |
| if (!arguments.length) return color; |
| color = nv.utils.getColor(_); |
| return chart; |
| }; |
| |
| chart.shape = function(_) { |
| if (!arguments.length) return getShape; |
| getShape = _; |
| return chart; |
| }; |
| |
| chart.onlyCircles = function(_) { |
| if (!arguments.length) return onlyCircles; |
| onlyCircles = _; |
| return chart; |
| }; |
| |
| chart.id = function(_) { |
| if (!arguments.length) return id; |
| id = _; |
| return chart; |
| }; |
| |
| chart.singlePoint = function(_) { |
| if (!arguments.length) return singlePoint; |
| singlePoint = _; |
| return chart; |
| }; |
| |
| //============================================================ |
| |
| |
| return chart; |
| } |