/**
* 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;
};