dc.js Source: scatter-plot.js (original) (raw)
/**
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 tofalse
. - @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);
};