You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nvd3/src/models/scatter.js

689 lines
23 KiB
JavaScript

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
, transitionDuration = 250
;
//============================================================
//============================================================
// 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 = data.map(function(series, i) {
series.values = series.values.map(function(point) {
point.series = i;
return point;
});
return series;
});
//------------------------------------------------------------
// 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()
.transition().duration(transitionDuration)
.style('stroke-opacity', 1e-6)
.style('fill-opacity', 1e-6)
.remove();
groups
.attr('class', function(d,i) { return 'nv-group nv-series-' + i })
.classed('hover', function(d) { return d.hover });
groups
.transition().duration(transitionDuration)
.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()
.duration(transitionDuration)
.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()
.duration(transitionDuration)
.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().duration(transitionDuration)
.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()
.duration(transitionDuration)
.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.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;
};
chart.transitionDuration = function(_) {
if (!arguments.length) return transitionDuration;
transitionDuration = _;
return chart;
};
//============================================================
return chart;
}