nvd3-fork
Version:
FORK! of NVD3, a reusable charting library written in d3.js
726 lines (645 loc) • 36.4 kB
JavaScript
nv.models.scatter = function() {
"use strict";
//============================================================
// Public Variables with Default Settings
//------------------------------------------------------------
var margin = {top: 0, right: 0, bottom: 0, left: 0}
, width = null
, height = null
, color = nv.utils.defaultColor() // chooses color
, pointBorderColor = null
, id = Math.floor(Math.random() * 100000) //Create semi-unique ID incase user doesn't select one
, container = null
, 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
, 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
, 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
, showVoronoi = false // display the voronoi areas
, 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', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'renderEnd')
, useVoronoi = true
, duration = 250
, interactiveUpdateDelay = 300
, showLabels = false
;
//============================================================
// Private Variables
//------------------------------------------------------------
var x0, y0, z0 // used to store previous scales
, xDom, yDom // used to store previous domains
, width0
, height0
, timeoutID
, needsUpdate = false // Flag for when the points are visually updating, but the interactive layer is behind, to disable tooltips
, renderWatch = nv.utils.renderWatch(dispatch, duration)
, _sizeRange_def = [16, 256]
, _cache = {}
;
//============================================================
// Diff and Cache Utilities
//------------------------------------------------------------
// getDiffs is used to filter unchanged points from the update
// selection. It implicitly updates it's cache when called and
// therefor the diff is based upon the previous invocation NOT
// the previous update.
//
// getDiffs takes a point as its first argument followed by n
// key getter pairs (d, [key, get... key, get]) this approach
// was chosen for efficiency. (The filter will call it a LOT).
//
// It is important to call delCache on point exit to prevent a
// memory leak. It is also needed to prevent invalid caches if
// a new point uses the same series and point id key.
//
// Argument Performance Concerns:
// - Object property lists for key getter pairs would be very
// expensive (points * objects for the GC every update).
// - ES6 function names for implicit keys would be nice but
// they are not guaranteed to be unique.
// - function.toString to obtain implicit keys is possible
// but long object keys are not free (internal hash).
// - Explicit key without objects are the most efficient.
function getCache(d) {
var key, val;
key = d[0].series + ':' + d[1];
val = _cache[key] = _cache[key] || {};
return val;
}
function delCache(d) {
var key, val;
key = d[0].series + ':' + d[1];
delete _cache[key];
}
function getDiffs(d) {
var i, key, val,
cache = getCache(d),
diffs = false;
for (i = 1; i < arguments.length; i += 2) {
key = arguments[i];
val = arguments[i + 1](d[0], d[1]);
if (cache[key] !== val || !cache.hasOwnProperty(key)) {
cache[key] = val;
diffs = true;
}
}
return diffs;
}
function chart(selection) {
renderWatch.reset();
selection.each(function(data) {
container = d3.select(this);
var availableWidth = nv.utils.availableWidth(width, container, margin),
availableHeight = nv.utils.availableHeight(height, container, margin);
nv.utils.initSVG(container);
//add series index to each data point for reference
data.forEach(function(series, i) {
series.values.forEach(function(point) {
point.series = i;
});
});
// Setup Scales
var logScale = (typeof(chart.yScale().base) === "function"); // Only log scale has a method "base()"
// 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]);
if (logScale) {
var min = d3.min(seriesData.map(function(d) { if (d.y !== 0) return d.y; }));
y.clamp(true)
.domain(yDomain || d3.extent(seriesData.map(function(d) {
if (d.y !== 0) return d.y;
else return min * 0.1;
}).concat(forceY)))
.range(yRange || [availableHeight, 0]);
} else {
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 || _sizeRange_def);
// If scale's domain don't have a range, slightly adjust to make one... so a chart can show a single data point
singlePoint = x.domain()[0] === x.domain()[1] || y.domain()[0] === y.domain()[1];
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;
var scaleDiff = x(1) !== x0(1) || y(1) !== y0(1) || z(1) !== z0(1);
width0 = width0 || width;
height0 = height0 || height;
var sizeDiff = width0 !== width || height0 !== height;
// Domain Diffs
xDom = xDom || [];
var domainDiff = xDom[0] !== x.domain()[0] || xDom[1] !== x.domain()[1];
xDom = x.domain();
yDom = yDom || [];
domainDiff = domainDiff || yDom[0] !== y.domain()[0] || yDom[1] !== y.domain()[1];
yDom = y.domain();
// 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);
var defsEnter = wrapEnter.append('defs');
var gEnter = wrapEnter.append('g');
var g = wrap.select('g');
wrap.classed('nv-single-point', singlePoint);
gEnter.append('g').attr('class', 'nv-groups');
gEnter.append('g').attr('class', 'nv-point-paths');
wrapEnter.append('g').attr('class', 'nv-point-clips');
wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
defsEnter.append('clipPath')
.attr('id', 'nv-edge-clip-' + id)
.append('rect')
.attr('transform', 'translate( -10, -10)');
wrap.select('#nv-edge-clip-' + id + ' rect')
.attr('width', availableWidth + 20)
.attr('height', (availableHeight > 0) ? availableHeight + 20 : 0);
g.attr('clip-path', clipEdge ? 'url(#nv-edge-clip-' + id + ')' : '');
function updateInteractiveLayer() {
// Always clear needs-update flag regardless of whether or not
// we will actually do anything (avoids needless invocations).
needsUpdate = false;
if (!interactive) return false;
container.selectAll(".nv-point.hover").classed("hover", false);
// nuke all voronoi paths
wrap.select('.nv-point-paths').selectAll('path').remove();
// inject series and point index for reference into voronoi
if (useVoronoi === true) {
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 [nv.utils.NaNtoZero(x(pX)) + Math.random() * 1e-4,
nv.utils.NaNtoZero(y(pY)) + Math.random() * 1e-4,
groupIndex,
pointIndex, point];
})
.filter(function(pointArray, pointIndex) {
return pointActive(pointArray[4], pointIndex); // Issue #237.. move filter to after map, so pointIndex is correct!
})
})
);
if (vertices.length == 0) return false; // No active points, we're done
if (vertices.length < 3) {
// 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]);
}
// keep voronoi sections from going more than 10 outside of graph
// to avoid overlap with other things like legend etc
var bounds = d3.geom.polygon([
[-10,-10],
[-10,height + 10],
[width + 10,height + 10],
[width + 10,-10]
]);
// delete duplicates from vertices - essential assumption for d3.geom.voronoi
var epsilon = 1e-4; // Uses 1e-4 to determine equivalence.
vertices = vertices.sort(function(a,b){return ((a[0] - b[0]) || (a[1] - b[1]))});
for (var i = 0; i < vertices.length - 1; ) {
if ((Math.abs(vertices[i][0] - vertices[i+1][0]) < epsilon) &&
(Math.abs(vertices[i][1] - vertices[i+1][1]) < epsilon)) {
vertices.splice(i+1, 1);
} else {
i++;
}
}
var voronoi = d3.geom.voronoi(vertices).map(function(d, i) {
if (d.length === 0) {
return null;
}
return {
'data': bounds.clip(d),
'series': vertices[i][2],
'point': vertices[i][3]
}
});
var pointPaths = wrap.select('.nv-point-paths').selectAll('path').data(voronoi);
var vPointPaths = pointPaths
.enter().append("svg:path")
.attr("d", function(d) {
if (!d || !d.data || d.data.length === 0)
return 'M 0 0';
else
return "M" + d.data.join(",") + "Z";
})
.attr("id", function(d,i) {
return "nv-path-"+i; })
.attr("clip-path", function(d,i) { return "url(#nv-clip-"+id+"-"+i+")"; })
;
// good for debugging point hover issues
if (showVoronoi) {
vPointPaths.style("fill", d3.rgb(230, 230, 230))
.style('fill-opacity', 0.4)
.style('stroke-opacity', 1)
.style("stroke", d3.rgb(200,200,200));
}
if (clipVoronoi) {
// voronoi sections are already set to clip,
// just create the circles with the IDs they expect
wrap.select('.nv-point-clips').selectAll('*').remove(); // must do * since it has sub-dom
var pointClips = wrap.select('.nv-point-clips').selectAll('clipPath').data(vertices);
var vPointClips = pointClips
.enter().append("svg:clipPath")
.attr("id", function(d, i) { return "nv-clip-"+id+"-"+i;})
.append("svg:circle")
.attr('cx', function(d) { return d[0]; })
.attr('cy', function(d) { return d[1]; })
.attr('r', clipRadius);
}
var mouseEventCallback = function(el, d, mDispatch) {
if (needsUpdate) return 0;
var series = data[d.series];
if (series === undefined) return;
var point = series.values[d.point];
point['color'] = color(series, d.series);
// standardize attributes for tooltip.
point['x'] = getX(point);
point['y'] = getY(point);
// can't just get box of event node since it's actually a voronoi polygon
var box = container.node().getBoundingClientRect();
var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
var scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
var pos = {
left: x(getX(point, d.point)) + box.left + scrollLeft + margin.left + 10,
top: y(getY(point, d.point)) + box.top + scrollTop + margin.top + 10
};
mDispatch({
point: point,
series: series,
pos: pos,
relativePos: [x(getX(point, d.point)) + margin.left, y(getY(point, d.point)) + margin.top],
seriesIndex: d.series,
pointIndex: d.point,
event: d3.event,
element: el
});
};
pointPaths
.on('click', function(d) {
mouseEventCallback(this, d, dispatch.elementClick);
})
.on('dblclick', function(d) {
mouseEventCallback(this, d, dispatch.elementDblClick);
})
.on('mouseover', function(d) {
mouseEventCallback(this, d, dispatch.elementMouseover);
})
.on('mouseout', function(d, i) {
mouseEventCallback(this, d, dispatch.elementMouseout);
});
} else {
// 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[0].series]) return 0; //check if this is a dummy point
var series = data[d[0].series],
point = series.values[i];
var element = this;
dispatch.elementClick({
point: point,
series: series,
pos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top], //TODO: make this pos base on the page
relativePos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top],
seriesIndex: d[0].series,
pointIndex: i,
event: d3.event,
element: element
});
})
.on('dblclick', function(d,i) {
if (needsUpdate || !data[d[0].series]) return 0; //check if this is a dummy point
var series = data[d[0].series],
point = series.values[i];
dispatch.elementDblClick({
point: point,
series: series,
pos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top],//TODO: make this pos base on the page
relativePos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top],
seriesIndex: d[0].series,
pointIndex: i
});
})
.on('mouseover', function(d,i) {
if (needsUpdate || !data[d[0].series]) return 0; //check if this is a dummy point
var series = data[d[0].series],
point = series.values[i];
dispatch.elementMouseover({
point: point,
series: series,
pos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top],//TODO: make this pos base on the page
relativePos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top],
seriesIndex: d[0].series,
pointIndex: i,
color: color(d[0], i)
});
})
.on('mouseout', function(d,i) {
if (needsUpdate || !data[d[0].series]) return 0; //check if this is a dummy point
var series = data[d[0].series],
point = series.values[i];
dispatch.elementMouseout({
point: point,
series: series,
pos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top],//TODO: make this pos base on the page
relativePos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top],
seriesIndex: d[0].series,
pointIndex: i,
color: color(d[0], i)
});
});
}
}
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 (d.classed || '') + ' nv-group nv-series-' + i;
})
.classed('nv-noninteractive', !interactive)
.classed('hover', function(d) { return d.hover });
groups.watchTransition(renderWatch, 'scatter: groups')
.style('fill', function(d,i) { return color(d, i) })
.style('stroke', function(d,i) { return d.pointBorderColor || pointBorderColor || color(d, i) })
.style('stroke-opacity', 1)
.style('fill-opacity', .5);
// create the points, maintaining their IDs from the original data set
var points = groups.selectAll('path.nv-point')
.data(function(d) {
return d.values.map(
function (point, pointIndex) {
return [point, pointIndex]
}).filter(
function(pointArray, pointIndex) {
return pointActive(pointArray[0], pointIndex)
})
});
points.enter().append('path')
.attr('class', function (d) {
return 'nv-point nv-point-' + d[1];
})
.style('fill', function (d) { return d.color })
.style('stroke', function (d) { return d.color })
.attr('transform', function(d) {
return 'translate(' + nv.utils.NaNtoZero(x0(getX(d[0],d[1]))) + ',' + nv.utils.NaNtoZero(y0(getY(d[0],d[1]))) + ')'
})
.attr('d',
nv.utils.symbol()
.type(function(d) { return getShape(d[0]); })
.size(function(d) { return z(getSize(d[0],d[1])) })
);
points.exit().each(delCache).remove();
groups.exit().selectAll('path.nv-point')
.watchTransition(renderWatch, 'scatter exit')
.attr('transform', function(d) {
return 'translate(' + nv.utils.NaNtoZero(x(getX(d[0],d[1]))) + ',' + nv.utils.NaNtoZero(y(getY(d[0],d[1]))) + ')'
})
.remove();
//============================================================
// Point Update Optimisation Notes
//------------------------------------------------------------
// The following update selections are filtered with getDiffs
// (defined at the top of this file) this brings a performance
// benefit for charts with large data sets that accumulate a
// subset of changes or additions over time.
//
// Uneccesary and expensive DOM calls are avoided by culling
// unchanged points from the selection in exchange for the
// cheaper overhead of caching and diffing each point first.
//
// Due to the way D3 and NVD3 work, other global changes need
// to be considered in addition to local point properties.
// This is a potential source of bugs (if any of the global
// changes that possibly affect points are missed).
// Update Point Positions [x, y]
points.filter(function (d) {
// getDiffs must always be called to update cache
return getDiffs(d, 'x', getX, 'y', getY) ||
scaleDiff || sizeDiff || domainDiff;
})
.watchTransition(renderWatch, 'scatter points')
.attr('transform', function (d) {
return 'translate(' +
nv.utils.NaNtoZero(x(getX(d[0], d[1]))) + ',' +
nv.utils.NaNtoZero(y(getY(d[0], d[1]))) + ')'
});
// Update Point Appearance [shape, size]
points.filter(function (d) {
// getDiffs must always be called to update cache
return getDiffs(d, 'shape', getShape, 'size', getSize) ||
scaleDiff || sizeDiff || domainDiff;
})
.watchTransition(renderWatch, 'scatter points')
.attr('d', nv.utils.symbol()
.type(function (d) { return getShape(d[0]) })
.size(function (d) { return z(getSize(d[0], d[1])) })
);
// add label a label to scatter chart
if(showLabels)
{
var titles = groups.selectAll('.nv-label')
.data(function(d) {
return d.values.map(
function (point, pointIndex) {
return [point, pointIndex]
}).filter(
function(pointArray, pointIndex) {
return pointActive(pointArray[0], pointIndex)
})
});
titles.enter().append('text')
.style('fill', function (d,i) {
return d.color })
.style('stroke-opacity', 0)
.style('fill-opacity', 1)
.attr('transform', function(d) {
var dx = nv.utils.NaNtoZero(x0(getX(d[0],d[1]))) + Math.sqrt(z(getSize(d[0],d[1]))/Math.PI) + 2;
return 'translate(' + dx + ',' + nv.utils.NaNtoZero(y0(getY(d[0],d[1]))) + ')';
})
.text(function(d,i){
return d[0].label;});
titles.exit().remove();
groups.exit().selectAll('path.nv-label')
.watchTransition(renderWatch, 'scatter exit')
.attr('transform', function(d) {
var dx = nv.utils.NaNtoZero(x(getX(d[0],d[1])))+ Math.sqrt(z(getSize(d[0],d[1]))/Math.PI)+2;
return 'translate(' + dx + ',' + nv.utils.NaNtoZero(y(getY(d[0],d[1]))) + ')';
})
.remove();
titles.each(function(d) {
d3.select(this)
.classed('nv-label', true)
.classed('nv-label-' + d[1], false)
.classed('hover',false);
});
titles.watchTransition(renderWatch, 'scatter labels')
.text(function(d,i){
return d[0].label;})
.attr('transform', function(d) {
var dx = nv.utils.NaNtoZero(x(getX(d[0],d[1])))+ Math.sqrt(z(getSize(d[0],d[1]))/Math.PI)+2;
return 'translate(' + dx + ',' + nv.utils.NaNtoZero(y(getY(d[0],d[1]))) + ')'
});
}
// Delay updating the invisible interactive layer for smoother animation
if( interactiveUpdateDelay )
{
clearTimeout(timeoutID); // stop repeat calls to updateInteractiveLayer
timeoutID = setTimeout(updateInteractiveLayer, interactiveUpdateDelay );
}
else
{
updateInteractiveLayer();
}
//store old scales for use in transitions on update
x0 = x.copy();
y0 = y.copy();
z0 = z.copy();
width0 = width;
height0 = height;
});
renderWatch.renderEnd('scatter immediate');
return chart;
}
//============================================================
// Expose Public Variables
//------------------------------------------------------------
chart.dispatch = dispatch;
chart.options = nv.utils.optionsFunc.bind(chart);
// utility function calls provided by this chart
chart._calls = new function() {
this.clearHighlights = function () {
nv.dom.write(function() {
container.selectAll(".nv-point.hover").classed("hover", false);
});
return null;
};
this.highlightPoint = function (seriesIndex, pointIndex, isHoverOver) {
nv.dom.write(function() {
container.select('.nv-groups')
.selectAll(".nv-series-" + seriesIndex)
.selectAll(".nv-point-" + pointIndex)
.classed("hover", isHoverOver);
});
};
};
// trigger calls from events too
dispatch.on('elementMouseover.point', function(d) {
if (interactive) chart._calls.highlightPoint(d.seriesIndex,d.pointIndex,true);
});
dispatch.on('elementMouseout.point', function(d) {
if (interactive) chart._calls.highlightPoint(d.seriesIndex,d.pointIndex,false);
});
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=_;}},
xScale: {get: function(){return x;}, set: function(_){x=_;}},
yScale: {get: function(){return y;}, set: function(_){y=_;}},
pointScale: {get: function(){return z;}, set: function(_){z=_;}},
xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}},
yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}},
pointDomain: {get: function(){return sizeDomain;}, set: function(_){sizeDomain=_;}},
xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}},
yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}},
pointRange: {get: function(){return sizeRange;}, set: function(_){sizeRange=_;}},
forceX: {get: function(){return forceX;}, set: function(_){forceX=_;}},
forceY: {get: function(){return forceY;}, set: function(_){forceY=_;}},
forcePoint: {get: function(){return forceSize;}, set: function(_){forceSize=_;}},
interactive: {get: function(){return interactive;}, set: function(_){interactive=_;}},
pointActive: {get: function(){return pointActive;}, set: function(_){pointActive=_;}},
padDataOuter: {get: function(){return padDataOuter;}, set: function(_){padDataOuter=_;}},
padData: {get: function(){return padData;}, set: function(_){padData=_;}},
clipEdge: {get: function(){return clipEdge;}, set: function(_){clipEdge=_;}},
clipVoronoi: {get: function(){return clipVoronoi;}, set: function(_){clipVoronoi=_;}},
clipRadius: {get: function(){return clipRadius;}, set: function(_){clipRadius=_;}},
showVoronoi: {get: function(){return showVoronoi;}, set: function(_){showVoronoi=_;}},
id: {get: function(){return id;}, set: function(_){id=_;}},
interactiveUpdateDelay: {get:function(){return interactiveUpdateDelay;}, set: function(_){interactiveUpdateDelay=_;}},
showLabels: {get: function(){return showLabels;}, set: function(_){ showLabels = _;}},
pointBorderColor: {get: function(){return pointBorderColor;}, set: function(_){pointBorderColor=_;}},
// simple functor options
x: {get: function(){return getX;}, set: function(_){getX = d3.functor(_);}},
y: {get: function(){return getY;}, set: function(_){getY = d3.functor(_);}},
pointSize: {get: function(){return getSize;}, set: function(_){getSize = d3.functor(_);}},
pointShape: {get: function(){return getShape;}, set: function(_){getShape = d3.functor(_);}},
// options that require extra logic in the setter
margin: {get: function(){return margin;}, set: function(_){
margin.top = _.top !== undefined ? _.top : margin.top;
margin.right = _.right !== undefined ? _.right : margin.right;
margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
margin.left = _.left !== undefined ? _.left : margin.left;
}},
duration: {get: function(){return duration;}, set: function(_){
duration = _;
renderWatch.reset(duration);
}},
color: {get: function(){return color;}, set: function(_){
color = nv.utils.getColor(_);
}},
useVoronoi: {get: function(){return useVoronoi;}, set: function(_){
useVoronoi = _;
if (useVoronoi === false) {
clipVoronoi = false;
}
}}
});
nv.utils.initOptions(chart);
return chart;
};