/**
* A scatter plot chart
*
* Examples:
* - {@link http://dc-js.github.io/dc.js/examples/scatter.html Scatter Chart}
* - {@link http://dc-js.github.io/dc.js/examples/multi-scatter.html Multi-Scatter Chart}
* @class scatterPlot
* @memberof dc
* @mixes dc.coordinateGridMixin
* @example
* // create a scatter plot under #chart-container1 element using the default global chart group
* var chart1 = dc.scatterPlot('#chart-container1');
* // create a scatter plot under #chart-container2 element using chart group A
* var chart2 = dc.scatterPlot('#chart-container2', 'chartGroupA');
* // create a sub-chart under a composite parent chart
* var chart3 = dc.scatterPlot(compositeChart);
* @param {String|node|d3.selection} parent - Any valid
* {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector} specifying
* a dom block element such as a div; or a dom element or d3 selection.
* @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
* Interaction with a chart will only trigger events and redraws within the chart's group.
* @returns {dc.scatterPlot}
*/
dc.scatterPlot = function (parent, chartGroup) {
var _chart = dc.coordinateGridMixin({});
var _symbol = d3.symbol();
var _existenceAccessor = function (d) { return d.value; };
var originalKeyAccessor = _chart.keyAccessor();
_chart.keyAccessor(function (d) { return originalKeyAccessor(d)[0]; });
_chart.valueAccessor(function (d) { return originalKeyAccessor(d)[1]; });
_chart.colorAccessor(function () { return _chart._groupName; });
_chart.title(function (d) {
// this basically just counteracts the setting of its own key/value accessors
// see https://github.com/dc-js/dc.js/issues/702
return _chart.keyAccessor()(d) + ',' + _chart.valueAccessor()(d) + ': ' +
_chart.existenceAccessor()(d);
});
var _locator = function (d) {
return 'translate(' + _chart.x()(_chart.keyAccessor()(d)) + ',' +
_chart.y()(_chart.valueAccessor()(d)) + ')';
};
var _highlightedSize = 7;
var _symbolSize = 5;
var _excludedSize = 3;
var _excludedColor = null;
var _excludedOpacity = 1.0;
var _emptySize = 0;
var _emptyOpacity = 0;
var _nonemptyOpacity = 1;
var _emptyColor = null;
var _filtered = [];
var _canvas = null;
var _context = null;
var _useCanvas = false;
// Calculates element radius for canvas plot to be comparable to D3 area based symbol sizes
function canvasElementSize (d, isFiltered) {
if (!_existenceAccessor(d)) {
return _emptySize / Math.sqrt(Math.PI);
} else if (isFiltered) {
return _symbolSize / Math.sqrt(Math.PI);
} else {
return _excludedSize / Math.sqrt(Math.PI);
}
}
// Use a 2 dimensional brush
_chart.brush(d3.brush());
function elementSize (d, i) {
if (!_existenceAccessor(d)) {
return Math.pow(_emptySize, 2);
} else if (_filtered[i]) {
return Math.pow(_symbolSize, 2);
} else {
return Math.pow(_excludedSize, 2);
}
}
_symbol.size(elementSize);
dc.override(_chart, '_filter', function (filter) {
if (!arguments.length) {
return _chart.__filter();
}
return _chart.__filter(dc.filters.RangedTwoDimensionalFilter(filter));
});
_chart._resetSvgOld = _chart.resetSvg; // Copy original closure from base-mixin
/**
* Method that replaces original resetSvg and appropriately inserts canvas
* element along with svg element and sets their CSS properties appropriately
* so they are overlapped on top of each other.
* Remove the chart's SVGElements from the dom and recreate the container SVGElement.
* @method resetSvg
* @memberof dc.scatterPlot
* @instance
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/SVGElement SVGElement}
* @returns {SVGElement}
*/
_chart.resetSvg = function () {
if (!_useCanvas) {
return _chart._resetSvgOld();
} else {
_chart._resetSvgOld(); // Perform original svgReset inherited from baseMixin
_chart.select('canvas').remove(); // remove old canvas
var svgSel = _chart.svg();
var rootSel = _chart.root();
// Set root node to relative positioning and svg to absolute
rootSel.style('position', 'relative');
svgSel.style('position', 'relative');
// Check if SVG element already has any extra top/left CSS offsets
var svgLeft = isNaN(parseInt(svgSel.style('left'), 10)) ? 0 : parseInt(svgSel.style('left'), 10);
var svgTop = isNaN(parseInt(svgSel.style('top'), 10)) ? 0 : parseInt(svgSel.style('top'), 10);
var width = _chart.effectiveWidth();
var height = _chart.effectiveHeight();
var margins = _chart.margins(); // {top: 10, right: 130, bottom: 42, left: 42}
// Add the canvas element such that it perfectly overlaps the plot area of the scatter plot SVG
var devicePixelRatio = window.devicePixelRatio || 1;
_canvas = _chart.root().append('canvas')
.attr('x', 0)
.attr('y', 0)
.attr('width', (width) * devicePixelRatio)
.attr('height', (height) * devicePixelRatio)
.style('width', width + 'px')
.style('height', height + 'px')
.style('position', 'absolute')
.style('top', margins.top + svgTop + 'px')
.style('left', margins.left + svgLeft + 'px')
.style('z-index', -1) // Place behind SVG
.style('pointer-events', 'none'); // Disable pointer events on canvas so SVG can capture brushing
// Define canvas context and set clipping path
_context = _canvas.node().getContext('2d');
_context.scale(devicePixelRatio, devicePixelRatio);
_context.rect(0, 0, width, height);
_context.clip(); // Setup clipping path
_context.imageSmoothingQuality = 'high';
return _chart.svg(); // Respect original return param for _chart.resetSvg;
}
};
_chart.resizeCanvas = function () {
var width = _chart.effectiveWidth();
var height = _chart.effectiveHeight();
var devicePixelRatio = window.devicePixelRatio || 1;
_canvas
.attr('width', (width) * devicePixelRatio)
.attr('height', (height) * devicePixelRatio)
.style('width', width + 'px')
.style('height', height + 'px');
_context.scale(devicePixelRatio, devicePixelRatio);
};
/**
* Set or get whether to use canvas backend for plotting scatterPlot. Note that the
* canvas backend does not currently support
* {@link dc.scatterPlot#customSymbol customSymbol} or
* {@link dc.scatterPlot#symbol symbol} methods and is limited to always plotting
* with filled circles. Symbols are drawn with
* {@link dc.scatterPlot#symbolSize symbolSize} radius. By default, the SVG backend
* is used when `useCanvas` is set to `false`.
* @method useCanvas
* @memberof dc.scatterPlot
* @instance
* @param {Boolean} [useCanvas=false]
* @return {Boolean|d3.selection}
*/
_chart.useCanvas = function (useCanvas) {
if (!arguments.length) {
return _useCanvas;
}
_useCanvas = useCanvas;
return _chart;
};
/**
* Set or get canvas element. You should usually only ever use the get method as
* dc.js will handle canvas element generation. Provides valid canvas only when
* {@link dc.scatterPlot#useCanvas useCanvas} is set to `true`
* @method canvas
* @memberof dc.scatterPlot
* @instance
* @param {CanvasElement|d3.selection} [canvasElement]
* @return {CanvasElement|d3.selection}
*/
_chart.canvas = function (canvasElement) {
if (!arguments.length) {
return _canvas;
}
_canvas = canvasElement;
return _chart;
};
/**
* Get canvas 2D context. Provides valid context only when
* {@link dc.scatterPlot#useCanvas useCanvas} is set to `true`
* @method context
* @memberof dc.scatterPlot
* @instance
* @return {CanvasContext}
*/
_chart.context = function () {
return _context;
};
/*eslint complexity: [2,11] */
// Plots data on canvas element. If argument provided, assumes legend is
// currently being highlighted and modifies opacity/size of symbols accordingly
// @param {Object} [legendHighlightDatum] - Datum provided to legendHighlight method
function plotOnCanvas (legendHighlightDatum) {
_chart.resizeCanvas();
var context = _chart.context();
context.clearRect(0, 0, (context.canvas.width + 2) * 1, (context.canvas.height + 2) * 1);
var data = _chart.data();
// Draw the data on canvas
data.forEach(function (d, i) {
var isFiltered = !_chart.filter() || _chart.filter().isFiltered([d.key[0], d.key[1]]);
// Calculate opacity for current data point
var cOpacity = 1;
if (!_existenceAccessor(d)) {
cOpacity = _emptyOpacity;
} else if (isFiltered) {
cOpacity = _nonemptyOpacity;
} else {
cOpacity = _chart.excludedOpacity();
}
// Calculate color for current data point
var cColor = null;
if (_emptyColor && !_existenceAccessor(d)) {
cColor = _emptyColor;
} else if (_chart.excludedColor() && !isFiltered) {
cColor = _chart.excludedColor();
} else {
cColor = _chart.getColor(d);
}
var cSize = canvasElementSize(d, isFiltered);
// Adjust params for data points if legend is highlighted
if (legendHighlightDatum) {
var isHighlighted = (cColor === legendHighlightDatum.color);
// Calculate opacity for current data point
var fadeOutOpacity = 0.1; // TODO: Make this programmatically setable
if (!isHighlighted) { // Fade out non-highlighted colors + highlighted colors outside filter
cOpacity = fadeOutOpacity;
}
if (isHighlighted) { // Set size for highlighted color data points
cSize = _highlightedSize / Math.sqrt(Math.PI);
}
}
// Draw point on canvas
context.save();
context.globalAlpha = cOpacity;
context.beginPath();
context.arc(_chart.x()(_chart.keyAccessor()(d)), _chart.y()(_chart.valueAccessor()(d)), cSize, 0, 2 * Math.PI, true);
context.fillStyle = cColor;
context.fill();
// context.lineWidth = 0.5; // Commented out code to add stroke around scatter points if desired
// context.strokeStyle = '#333';
// context.stroke();
context.restore();
});
}
function plotOnSVG () {
var symbols = _chart.chartBodyG().selectAll('path.symbol')
.data(_chart.data());
dc.transition(symbols.exit(), _chart.transitionDuration(), _chart.transitionDelay())
.attr('opacity', 0).remove();
symbols = symbols
.enter()
.append('path')
.attr('class', 'symbol')
.attr('opacity', 0)
.attr('fill', _chart.getColor)
.attr('transform', _locator)
.merge(symbols);
symbols.call(renderTitles, _chart.data());
symbols.each(function (d, i) {
_filtered[i] = !_chart.filter() || _chart.filter().isFiltered([_chart.keyAccessor()(d), _chart.valueAccessor()(d)]);
});
dc.transition(symbols, _chart.transitionDuration(), _chart.transitionDelay())
.attr('opacity', function (d, i) {
if (!_existenceAccessor(d)) {
return _emptyOpacity;
} else if (_filtered[i]) {
return _nonemptyOpacity;
} else {
return _chart.excludedOpacity();
}
})
.attr('fill', function (d, i) {
if (_emptyColor && !_existenceAccessor(d)) {
return _emptyColor;
} else if (_chart.excludedColor() && !_filtered[i]) {
return _chart.excludedColor();
} else {
return _chart.getColor(d);
}
})
.attr('transform', _locator)
.attr('d', _symbol);
}
_chart.plotData = function () {
if (_useCanvas) {
plotOnCanvas();
} else {
plotOnSVG();
}
};
function renderTitles (symbol, d) {
if (_chart.renderTitle()) {
symbol.selectAll('title').remove();
symbol.append('title').text(function (d) {
return _chart.title()(d);
});
}
}
/**
* Get or set the existence accessor. If a point exists, it is drawn with
* {@link dc.scatterPlot#symbolSize symbolSize} radius and
* opacity 1; if it does not exist, it is drawn with
* {@link dc.scatterPlot#emptySize emptySize} radius and opacity 0. By default,
* the existence accessor checks if the reduced value is truthy.
* @method existenceAccessor
* @memberof dc.scatterPlot
* @instance
* @see {@link dc.scatterPlot#symbolSize symbolSize}
* @see {@link dc.scatterPlot#emptySize emptySize}
* @example
* // default accessor
* chart.existenceAccessor(function (d) { return d.value; });
* @param {Function} [accessor]
* @returns {Function|dc.scatterPlot}
*/
_chart.existenceAccessor = function (accessor) {
if (!arguments.length) {
return _existenceAccessor;
}
_existenceAccessor = accessor;
return this;
};
/**
* Get or set the symbol type used for each point. By default the symbol is a circle (d3.symbolCircle).
* Type can be a constant or an accessor.
* @method symbol
* @memberof dc.scatterPlot
* @instance
* @see {@link https://github.com/d3/d3-shape/blob/master/README.md#symbol_type symbol.type}
* @example
* // Circle type
* chart.symbol(d3.symbolCircle);
* // Square type
* chart.symbol(d3.symbolSquare);
* @param {Function} [type=d3.symbolCircle]
* @returns {Function|dc.scatterPlot}
*/
_chart.symbol = function (type) {
if (!arguments.length) {
return _symbol.type();
}
_symbol.type(type);
return _chart;
};
/**
* Get or set the symbol generator. By default `dc.scatterPlot` will use
* {@link https://github.com/d3/d3-shape/blob/master/README.md#symbol d3.symbol()}
* to generate symbols. `dc.scatterPlot` will set the
* {@link https://github.com/d3/d3-shape/blob/master/README.md#symbol_size symbol size accessor}
* on the symbol generator.
* @method customSymbol
* @memberof dc.scatterPlot
* @instance
* @see {@link https://github.com/d3/d3-shape/blob/master/README.md#symbol d3.symbol}
* @see {@link https://stackoverflow.com/questions/25332120/create-additional-d3-js-symbols Create additional D3.js symbols}
* @param {String|Function} [customSymbol=d3.symbol()]
* @returns {String|Function|dc.scatterPlot}
*/
_chart.customSymbol = function (customSymbol) {
if (!arguments.length) {
return _symbol;
}
_symbol = customSymbol;
_symbol.size(elementSize);
return _chart;
};
/**
* Set or get radius for symbols.
* @method symbolSize
* @memberof dc.scatterPlot
* @instance
* @see {@link https://github.com/d3/d3-shape/blob/master/README.md#symbol_size d3.symbol.size}
* @param {Number} [symbolSize=3]
* @returns {Number|dc.scatterPlot}
*/
_chart.symbolSize = function (symbolSize) {
if (!arguments.length) {
return _symbolSize;
}
_symbolSize = symbolSize;
return _chart;
};
/**
* Set or get radius for highlighted symbols.
* @method highlightedSize
* @memberof dc.scatterPlot
* @instance
* @see {@link https://github.com/d3/d3-shape/blob/master/README.md#symbol_size d3.symbol.size}
* @param {Number} [highlightedSize=5]
* @returns {Number|dc.scatterPlot}
*/
_chart.highlightedSize = function (highlightedSize) {
if (!arguments.length) {
return _highlightedSize;
}
_highlightedSize = highlightedSize;
return _chart;
};
/**
* Set or get size for symbols excluded from this chart's filter. If null, no
* special size is applied for symbols based on their filter status.
* @method excludedSize
* @memberof dc.scatterPlot
* @instance
* @see {@link https://github.com/d3/d3-shape/blob/master/README.md#symbol_size d3.symbol.size}
* @param {Number} [excludedSize=null]
* @returns {Number|dc.scatterPlot}
*/
_chart.excludedSize = function (excludedSize) {
if (!arguments.length) {
return _excludedSize;
}
_excludedSize = excludedSize;
return _chart;
};
/**
* Set or get color for symbols excluded from this chart's filter. If null, no
* special color is applied for symbols based on their filter status.
* @method excludedColor
* @memberof dc.scatterPlot
* @instance
* @param {Number} [excludedColor=null]
* @returns {Number|dc.scatterPlot}
*/
_chart.excludedColor = function (excludedColor) {
if (!arguments.length) {
return _excludedColor;
}
_excludedColor = excludedColor;
return _chart;
};
/**
* Set or get opacity for symbols excluded from this chart's filter.
* @method excludedOpacity
* @memberof dc.scatterPlot
* @instance
* @param {Number} [excludedOpacity=1.0]
* @returns {Number|dc.scatterPlot}
*/
_chart.excludedOpacity = function (excludedOpacity) {
if (!arguments.length) {
return _excludedOpacity;
}
_excludedOpacity = excludedOpacity;
return _chart;
};
/**
* Set or get radius for symbols when the group is empty.
* @method emptySize
* @memberof dc.scatterPlot
* @instance
* @see {@link https://github.com/d3/d3-shape/blob/master/README.md#symbol_size d3.symbol.size}
* @param {Number} [emptySize=0]
* @returns {Number|dc.scatterPlot}
*/
_chart.hiddenSize = _chart.emptySize = function (emptySize) {
if (!arguments.length) {
return _emptySize;
}
_emptySize = emptySize;
return _chart;
};
/**
* Set or get color for symbols when the group is empty. If null, just use the
* {@link dc.colorMixin#colors colorMixin.colors} color scale zero value.
* @name emptyColor
* @memberof dc.scatterPlot
* @instance
* @param {String} [emptyColor=null]
* @return {String}
* @return {dc.scatterPlot}/
*/
_chart.emptyColor = function (emptyColor) {
if (!arguments.length) {
return _emptyColor;
}
_emptyColor = emptyColor;
return _chart;
};
/**
* Set or get opacity for symbols when the group is empty.
* @name emptyOpacity
* @memberof dc.scatterPlot
* @instance
* @param {Number} [emptyOpacity=0]
* @return {Number}
* @return {dc.scatterPlot}
*/
_chart.emptyOpacity = function (emptyOpacity) {
if (!arguments.length) {
return _emptyOpacity;
}
_emptyOpacity = emptyOpacity;
return _chart;
};
/**
* Set or get opacity for symbols when the group is not empty.
* @name nonemptyOpacity
* @memberof dc.scatterPlot
* @instance
* @param {Number} [nonemptyOpacity=1]
* @return {Number}
* @return {dc.scatterPlot}
*/
_chart.nonemptyOpacity = function (nonemptyOpacity) {
if (!arguments.length) {
return _emptyOpacity;
}
_nonemptyOpacity = nonemptyOpacity;
return _chart;
};
_chart.legendables = function () {
return [{chart: _chart, name: _chart._groupName, color: _chart.getColor()}];
};
_chart.legendHighlight = function (d) {
if (_useCanvas) {
plotOnCanvas(d); // Supply legend datum to plotOnCanvas
} else {
resizeSymbolsWhere(function (symbol) {
return symbol.attr('fill') === d.color;
}, _highlightedSize);
_chart.chartBodyG().selectAll('.chart-body path.symbol').filter(function () {
return d3.select(this).attr('fill') !== d.color;
}).classed('fadeout', true);
}
};
_chart.legendReset = function (d) {
if (_useCanvas) {
plotOnCanvas();
} else {
resizeSymbolsWhere(function (symbol) {
return symbol.attr('fill') === d.color;
}, _symbolSize);
_chart.chartBodyG().selectAll('.chart-body path.symbol').filter(function () {
return d3.select(this).attr('fill') !== d.color;
}).classed('fadeout', false);
}
};
function resizeSymbolsWhere (condition, size) {
var symbols = _chart.chartBodyG().selectAll('.chart-body path.symbol').filter(function () {
return condition(d3.select(this));
});
var oldSize = _symbol.size();
_symbol.size(Math.pow(size, 2));
dc.transition(symbols, _chart.transitionDuration(), _chart.transitionDelay()).attr('d', _symbol);
_symbol.size(oldSize);
}
_chart.createBrushHandlePaths = function () {
// no handle paths for poly-brushes
};
_chart.extendBrush = function (brushSelection) {
if (_chart.round()) {
brushSelection[0] = brushSelection[0].map(_chart.round());
brushSelection[1] = brushSelection[1].map(_chart.round());
}
return brushSelection;
};
_chart.brushIsEmpty = function (brushSelection) {
return !brushSelection || brushSelection[0][0] >= brushSelection[1][0] || brushSelection[0][1] >= brushSelection[1][1];
};
_chart._brushing = function () {
// Avoids infinite recursion (mutual recursion between range and focus operations)
// Source Event will be null when brush.move is called programmatically (see below as well).
if (!d3.event.sourceEvent) { return; }
// Ignore event if recursive event - i.e. not directly generated by user action (like mouse/touch etc.)
// In this case we are more worried about this handler causing brush move programmatically which will
// cause this handler to be invoked again with a new d3.event (and current event set as sourceEvent)
// This check avoids recursive calls
if (d3.event.sourceEvent.type && ['start', 'brush', 'end'].indexOf(d3.event.sourceEvent.type) !== -1) {
return;
}
var brushSelection = d3.event.selection;
// Testing with pixels is more reliable
var brushIsEmpty = _chart.brushIsEmpty(brushSelection);
if (brushSelection) {
brushSelection = brushSelection.map(function (point) {
return point.map(function (coord, i) {
var scale = i === 0 ? _chart.x() : _chart.y();
return scale.invert(coord);
});
});
brushSelection = _chart.extendBrush(brushSelection);
// The rounding process might have made brushSelection empty, so we need to recheck
brushIsEmpty = brushIsEmpty && _chart.brushIsEmpty(brushSelection);
}
_chart.redrawBrush(brushSelection, false);
var ranged2DFilter = brushIsEmpty ? null : dc.filters.RangedTwoDimensionalFilter(brushSelection);
dc.events.trigger(function () {
_chart.replaceFilter(ranged2DFilter);
_chart.redrawGroup();
}, dc.constants.EVENT_DELAY);
};
_chart.redrawBrush = function (brushSelection, doTransition) {
// override default x axis brush from parent chart
var _brush = _chart.brush();
var _gBrush = _chart.gBrush();
if (_chart.brushOn() && _gBrush) {
if (_chart.resizing()) {
_chart.setBrushExtents(doTransition);
}
if (!brushSelection) {
_gBrush
.call(_brush.move, brushSelection);
} else {
brushSelection = brushSelection.map(function (point) {
return point.map(function (coord, i) {
var scale = i === 0 ? _chart.x() : _chart.y();
return scale(coord);
});
});
var gBrush =
dc.optionalTransition(doTransition, _chart.transitionDuration(), _chart.transitionDelay())(_gBrush);
gBrush
.call(_brush.move, brushSelection);
}
}
_chart.fadeDeselectedArea(brushSelection);
};
_chart.setBrushY = function (gBrush) {
gBrush.call(_chart.brush().y(_chart.y()));
};
return _chart.anchor(parent, chartGroup);
};