/** * This Mixin provides reusable functionalities for any chart that needs to visualize data using bubbles. * @name bubbleMixin * @memberof dc * @mixin * @mixes dc.colorMixin * @param {Object} _chart * @returns {dc.bubbleMixin} */ dc.bubbleMixin = function (_chart) { var _maxBubbleRelativeSize = 0.3; var _minRadiusWithLabel = 10; var _sortBubbleSize = false; var _elasticRadius = false; _chart.BUBBLE_NODE_CLASS = 'node'; _chart.BUBBLE_CLASS = 'bubble'; _chart.MIN_RADIUS = 10; _chart = dc.colorMixin(_chart); _chart.renderLabel(true); _chart.data(function (group) { var data = group.all(); if (_sortBubbleSize) { // sort descending so smaller bubbles are on top var radiusAccessor = _chart.radiusValueAccessor(); data.sort(function (a, b) { return d3.descending(radiusAccessor(a), radiusAccessor(b)); }); } return data; }); var _r = d3.scaleLinear().domain([0, 100]); var _rValueAccessor = function (d) { return d.r; }; /** * Get or set the bubble radius scale. By default the bubble chart uses * {@link https://github.com/d3/d3-scale/blob/master/README.md#scaleLinear d3.scaleLinear().domain([0, 100])} * as its radius scale. * @method r * @memberof dc.bubbleMixin * @instance * @see {@link https://github.com/d3/d3-scale/blob/master/README.md d3.scale} * @param {d3.scale} [bubbleRadiusScale=d3.scaleLinear().domain([0, 100])] * @returns {d3.scale|dc.bubbleMixin} */ _chart.r = function (bubbleRadiusScale) { if (!arguments.length) { return _r; } _r = bubbleRadiusScale; return _chart; }; /** * Turn on or off the elastic bubble radius feature, or return the value of the flag. If this * feature is turned on, then bubble radii will be automatically rescaled to fit the chart better. * @method elasticRadius * @memberof dc.bubbleChart * @instance * @param {Boolean} [elasticRadius=false] * @returns {Boolean|dc.bubbleChart} */ _chart.elasticRadius = function (elasticRadius) { if (!arguments.length) { return _elasticRadius; } _elasticRadius = elasticRadius; return _chart; }; _chart.calculateRadiusDomain = function () { if (_elasticRadius) { _chart.r().domain([_chart.rMin(), _chart.rMax()]); } }; /** * Get or set the radius value accessor function. If set, the radius value accessor function will * be used to retrieve a data value for each bubble. The data retrieved then will be mapped using * the r scale to the actual bubble radius. This allows you to encode a data dimension using bubble * size. * @method radiusValueAccessor * @memberof dc.bubbleMixin * @instance * @param {Function} [radiusValueAccessor] * @returns {Function|dc.bubbleMixin} */ _chart.radiusValueAccessor = function (radiusValueAccessor) { if (!arguments.length) { return _rValueAccessor; } _rValueAccessor = radiusValueAccessor; return _chart; }; _chart.rMin = function () { var min = d3.min(_chart.data(), function (e) { return _chart.radiusValueAccessor()(e); }); return min; }; _chart.rMax = function () { var max = d3.max(_chart.data(), function (e) { return _chart.radiusValueAccessor()(e); }); return max; }; _chart.bubbleR = function (d) { var value = _chart.radiusValueAccessor()(d); var r = _chart.r()(value); if (isNaN(r) || value <= 0) { r = 0; } return r; }; var labelFunction = function (d) { return _chart.label()(d); }; var shouldLabel = function (d) { return (_chart.bubbleR(d) > _minRadiusWithLabel); }; var labelOpacity = function (d) { return shouldLabel(d) ? 1 : 0; }; var labelPointerEvent = function (d) { return shouldLabel(d) ? 'all' : 'none'; }; _chart._doRenderLabel = function (bubbleGEnter) { if (_chart.renderLabel()) { var label = bubbleGEnter.select('text'); if (label.empty()) { label = bubbleGEnter.append('text') .attr('text-anchor', 'middle') .attr('dy', '.3em') .on('click', _chart.onClick); } label .attr('opacity', 0) .attr('pointer-events', labelPointerEvent) .text(labelFunction); dc.transition(label, _chart.transitionDuration(), _chart.transitionDelay()) .attr('opacity', labelOpacity); } }; _chart.doUpdateLabels = function (bubbleGEnter) { if (_chart.renderLabel()) { var labels = bubbleGEnter.select('text') .attr('pointer-events', labelPointerEvent) .text(labelFunction); dc.transition(labels, _chart.transitionDuration(), _chart.transitionDelay()) .attr('opacity', labelOpacity); } }; var titleFunction = function (d) { return _chart.title()(d); }; _chart._doRenderTitles = function (g) { if (_chart.renderTitle()) { var title = g.select('title'); if (title.empty()) { g.append('title').text(titleFunction); } } }; _chart.doUpdateTitles = function (g) { if (_chart.renderTitle()) { g.select('title').text(titleFunction); } }; /** * Turn on or off the bubble sorting feature, or return the value of the flag. If enabled, * bubbles will be sorted by their radius, with smaller bubbles in front. * @method sortBubbleSize * @memberof dc.bubbleChart * @instance * @param {Boolean} [sortBubbleSize=false] * @returns {Boolean|dc.bubbleChart} */ _chart.sortBubbleSize = function (sortBubbleSize) { if (!arguments.length) { return _sortBubbleSize; } _sortBubbleSize = sortBubbleSize; return _chart; }; /** * Get or set the minimum radius. This will be used to initialize the radius scale's range. * @method minRadius * @memberof dc.bubbleMixin * @instance * @param {Number} [radius=10] * @returns {Number|dc.bubbleMixin} */ _chart.minRadius = function (radius) { if (!arguments.length) { return _chart.MIN_RADIUS; } _chart.MIN_RADIUS = radius; return _chart; }; /** * Get or set the minimum radius for label rendering. If a bubble's radius is less than this value * then no label will be rendered. * @method minRadiusWithLabel * @memberof dc.bubbleMixin * @instance * @param {Number} [radius=10] * @returns {Number|dc.bubbleMixin} */ _chart.minRadiusWithLabel = function (radius) { if (!arguments.length) { return _minRadiusWithLabel; } _minRadiusWithLabel = radius; return _chart; }; /** * Get or set the maximum relative size of a bubble to the length of x axis. This value is useful * when the difference in radius between bubbles is too great. * @method maxBubbleRelativeSize * @memberof dc.bubbleMixin * @instance * @param {Number} [relativeSize=0.3] * @returns {Number|dc.bubbleMixin} */ _chart.maxBubbleRelativeSize = function (relativeSize) { if (!arguments.length) { return _maxBubbleRelativeSize; } _maxBubbleRelativeSize = relativeSize; return _chart; }; _chart.fadeDeselectedArea = function (selection) { if (_chart.hasFilter()) { _chart.selectAll('g.' + _chart.BUBBLE_NODE_CLASS).each(function (d) { if (_chart.isSelectedNode(d)) { _chart.highlightSelected(this); } else { _chart.fadeDeselected(this); } }); } else { _chart.selectAll('g.' + _chart.BUBBLE_NODE_CLASS).each(function () { _chart.resetHighlight(this); }); } }; _chart.isSelectedNode = function (d) { return _chart.hasFilter(d.key); }; _chart.onClick = function (d) { var filter = d.key; dc.events.trigger(function () { _chart.filter(filter); _chart.redrawGroup(); }); }; return _chart; };