/** * The bubble overlay chart is quite different from the typical bubble chart. With the bubble overlay * chart you can arbitrarily place bubbles on an existing svg or bitmap image, thus changing the * typical x and y positioning while retaining the capability to visualize data using bubble radius * and coloring. * * Examples: * - {@link http://dc-js.github.com/dc.js/crime/index.html Canadian City Crime Stats} * @class bubbleOverlay * @memberof dc * @mixes dc.bubbleMixin * @mixes dc.baseMixin * @example * // create a bubble overlay chart on top of the '#chart-container1 svg' element using the default global chart group * var bubbleChart1 = dc.bubbleOverlayChart('#chart-container1').svg(d3.select('#chart-container1 svg')); * // create a bubble overlay chart on top of the '#chart-container2 svg' element using chart group A * var bubbleChart2 = dc.compositeChart('#chart-container2', 'chartGroupA').svg(d3.select('#chart-container2 svg')); * @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.bubbleOverlay} */ dc.bubbleOverlay = function (parent, chartGroup) { var BUBBLE_OVERLAY_CLASS = 'bubble-overlay'; var BUBBLE_NODE_CLASS = 'node'; var BUBBLE_CLASS = 'bubble'; /** * **mandatory** * * Set the underlying svg image element. Unlike other dc charts this chart will not generate a svg * element; therefore the bubble overlay chart will not work if this function is not invoked. If the * underlying image is a bitmap, then an empty svg will need to be created on top of the image. * @method svg * @memberof dc.bubbleOverlay * @instance * @example * // set up underlying svg element * chart.svg(d3.select('#chart svg')); * @param {SVGElement|d3.selection} [imageElement] * @returns {dc.bubbleOverlay} */ var _chart = dc.bubbleMixin(dc.baseMixin({})); var _g; var _points = []; _chart.transitionDuration(750); _chart.transitionDelay(0); _chart.radiusValueAccessor(function (d) { return d.value; }); /** * **mandatory** * * Set up a data point on the overlay. The name of a data point should match a specific 'key' among * data groups generated using keyAccessor. If a match is found (point name <-> data group key) * then a bubble will be generated at the position specified by the function. x and y * value specified here are relative to the underlying svg. * @method point * @memberof dc.bubbleOverlay * @instance * @param {String} name * @param {Number} x * @param {Number} y * @returns {dc.bubbleOverlay} */ _chart.point = function (name, x, y) { _points.push({name: name, x: x, y: y}); return _chart; }; _chart._doRender = function () { _g = initOverlayG(); _chart.r().range([_chart.MIN_RADIUS, _chart.width() * _chart.maxBubbleRelativeSize()]); initializeBubbles(); _chart.fadeDeselectedArea(_chart.filter()); return _chart; }; function initOverlayG () { _g = _chart.select('g.' + BUBBLE_OVERLAY_CLASS); if (_g.empty()) { _g = _chart.svg().append('g').attr('class', BUBBLE_OVERLAY_CLASS); } return _g; } function initializeBubbles () { var data = mapData(); _chart.calculateRadiusDomain(); _points.forEach(function (point) { var nodeG = getNodeG(point, data); var circle = nodeG.select('circle.' + BUBBLE_CLASS); if (circle.empty()) { circle = nodeG.append('circle') .attr('class', BUBBLE_CLASS) .attr('r', 0) .attr('fill', _chart.getColor) .on('click', _chart.onClick); } dc.transition(circle, _chart.transitionDuration(), _chart.transitionDelay()) .attr('r', function (d) { return _chart.bubbleR(d); }); _chart._doRenderLabel(nodeG); _chart._doRenderTitles(nodeG); }); } function mapData () { var data = {}; _chart.data().forEach(function (datum) { data[_chart.keyAccessor()(datum)] = datum; }); return data; } function getNodeG (point, data) { var bubbleNodeClass = BUBBLE_NODE_CLASS + ' ' + dc.utils.nameToId(point.name); var nodeG = _g.select('g.' + dc.utils.nameToId(point.name)); if (nodeG.empty()) { nodeG = _g.append('g') .attr('class', bubbleNodeClass) .attr('transform', 'translate(' + point.x + ',' + point.y + ')'); } nodeG.datum(data[point.name]); return nodeG; } _chart._doRedraw = function () { updateBubbles(); _chart.fadeDeselectedArea(_chart.filter()); return _chart; }; function updateBubbles () { var data = mapData(); _chart.calculateRadiusDomain(); _points.forEach(function (point) { var nodeG = getNodeG(point, data); var circle = nodeG.select('circle.' + BUBBLE_CLASS); dc.transition(circle, _chart.transitionDuration(), _chart.transitionDelay()) .attr('r', function (d) { return _chart.bubbleR(d); }) .attr('fill', _chart.getColor); _chart.doUpdateLabels(nodeG); _chart.doUpdateTitles(nodeG); }); } _chart.debug = function (flag) { if (flag) { var debugG = _chart.select('g.' + dc.constants.DEBUG_GROUP_CLASS); if (debugG.empty()) { debugG = _chart.svg() .append('g') .attr('class', dc.constants.DEBUG_GROUP_CLASS); } var debugText = debugG.append('text') .attr('x', 10) .attr('y', 20); debugG .append('rect') .attr('width', _chart.width()) .attr('height', _chart.height()) .on('mousemove', function () { var position = d3.mouse(debugG.node()); var msg = position[0] + ', ' + position[1]; debugText.text(msg); }); } else { _chart.selectAll('.debug').remove(); } return _chart; }; _chart.anchor(parent, chartGroup); return _chart; };