/** * The pie chart implementation is usually used to visualize a small categorical distribution. The pie * chart uses keyAccessor to determine the slices, and valueAccessor to calculate the size of each * slice relative to the sum of all values. Slices are ordered by {@link dc.baseMixin#ordering ordering} * which defaults to sorting by key. * * Examples: * - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index} * @class pieChart * @memberof dc * @mixes dc.capMixin * @mixes dc.colorMixin * @mixes dc.baseMixin * @example * // create a pie chart under #chart-container1 element using the default global chart group * var chart1 = dc.pieChart('#chart-container1'); * // create a pie chart under #chart-container2 element using chart group A * var chart2 = dc.pieChart('#chart-container2', 'chartGroupA'); * @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.pieChart} */ dc.pieChart = function (parent, chartGroup) { var DEFAULT_MIN_ANGLE_FOR_LABEL = 0.5; var _sliceCssClass = 'pie-slice'; var _labelCssClass = 'pie-label'; var _sliceGroupCssClass = 'pie-slice-group'; var _labelGroupCssClass = 'pie-label-group'; var _emptyCssClass = 'empty-chart'; var _emptyTitle = 'empty'; var _radius, _givenRadius, // specified radius, if any _innerRadius = 0, _externalRadiusPadding = 0; var _g; var _cx; var _cy; var _minAngleForLabel = DEFAULT_MIN_ANGLE_FOR_LABEL; var _externalLabelRadius; var _drawPaths = false; var _chart = dc.capMixin(dc.colorMixin(dc.baseMixin({}))); _chart.colorAccessor(_chart.cappedKeyAccessor); _chart.title(function (d) { return _chart.cappedKeyAccessor(d) + ': ' + _chart.cappedValueAccessor(d); }); /** * Get or set the maximum number of slices the pie chart will generate. The top slices are determined by * value from high to low. Other slices exeeding the cap will be rolled up into one single *Others* slice. * @method slicesCap * @memberof dc.pieChart * @instance * @param {Number} [cap] * @returns {Number|dc.pieChart} */ _chart.slicesCap = _chart.cap; _chart.label(_chart.cappedKeyAccessor); _chart.renderLabel(true); _chart.transitionDuration(350); _chart.transitionDelay(0); _chart._doRender = function () { _chart.resetSvg(); _g = _chart.svg() .append('g') .attr('transform', 'translate(' + _chart.cx() + ',' + _chart.cy() + ')'); _g.append('g').attr('class', _sliceGroupCssClass); _g.append('g').attr('class', _labelGroupCssClass); drawChart(); return _chart; }; function drawChart () { // set radius from chart size if none given, or if given radius is too large var maxRadius = d3.min([_chart.width(), _chart.height()]) / 2; _radius = _givenRadius && _givenRadius < maxRadius ? _givenRadius : maxRadius; var arc = buildArcs(); var pie = pieLayout(); var pieData; // if we have data... if (d3.sum(_chart.data(), _chart.cappedValueAccessor)) { pieData = pie(_chart.data()); _g.classed(_emptyCssClass, false); } else { // otherwise we'd be getting NaNs, so override // note: abuse others for its ignoring the value accessor pieData = pie([{key: _emptyTitle, value: 1, others: [_emptyTitle]}]); _g.classed(_emptyCssClass, true); } if (_g) { var slices = _g.select('g.' + _sliceGroupCssClass) .selectAll('g.' + _sliceCssClass) .data(pieData); var labels = _g.select('g.' + _labelGroupCssClass) .selectAll('text.' + _labelCssClass) .data(pieData); removeElements(slices, labels); createElements(slices, labels, arc, pieData); updateElements(pieData, arc); highlightFilter(); dc.transition(_g, _chart.transitionDuration(), _chart.transitionDelay()) .attr('transform', 'translate(' + _chart.cx() + ',' + _chart.cy() + ')'); } } function createElements (slices, labels, arc, pieData) { var slicesEnter = createSliceNodes(slices); createSlicePath(slicesEnter, arc); createTitles(slicesEnter); createLabels(labels, pieData, arc); } function createSliceNodes (slices) { var slicesEnter = slices .enter() .append('g') .attr('class', function (d, i) { return _sliceCssClass + ' _' + i; }); return slicesEnter; } function createSlicePath (slicesEnter, arc) { var slicePath = slicesEnter.append('path') .attr('fill', fill) .on('click', onClick) .attr('d', function (d, i) { return safeArc(d, i, arc); }); var transition = dc.transition(slicePath, _chart.transitionDuration(), _chart.transitionDelay()); if (transition.attrTween) { transition.attrTween('d', tweenPie); } } function createTitles (slicesEnter) { if (_chart.renderTitle()) { slicesEnter.append('title').text(function (d) { return _chart.title()(d.data); }); } } _chart._applyLabelText = function (labels) { labels .text(function (d) { var data = d.data; if ((sliceHasNoData(data) || sliceTooSmall(d)) && !isSelectedSlice(d)) { return ''; } return _chart.label()(d.data); }); }; function positionLabels (labels, arc) { _chart._applyLabelText(labels); dc.transition(labels, _chart.transitionDuration(), _chart.transitionDelay()) .attr('transform', function (d) { return labelPosition(d, arc); }) .attr('text-anchor', 'middle'); } function highlightSlice (i, whether) { _chart.select('g.pie-slice._' + i) .classed('highlight', whether); } function createLabels (labels, pieData, arc) { if (_chart.renderLabel()) { var labelsEnter = labels .enter() .append('text') .attr('class', function (d, i) { var classes = _sliceCssClass + ' ' + _labelCssClass + ' _' + i; if (_externalLabelRadius) { classes += ' external'; } return classes; }) .on('click', onClick) .on('mouseover', function (d, i) { highlightSlice(i, true); }) .on('mouseout', function (d, i) { highlightSlice(i, false); }); positionLabels(labelsEnter, arc); if (_externalLabelRadius && _drawPaths) { updateLabelPaths(pieData, arc); } } } function updateLabelPaths (pieData, arc) { var polyline = _g.selectAll('polyline.' + _sliceCssClass) .data(pieData); polyline.exit().remove(); polyline = polyline .enter() .append('polyline') .attr('class', function (d, i) { return 'pie-path _' + i + ' ' + _sliceCssClass; }) .on('click', onClick) .on('mouseover', function (d, i) { highlightSlice(i, true); }) .on('mouseout', function (d, i) { highlightSlice(i, false); }) .merge(polyline); var arc2 = d3.arc() .outerRadius(_radius - _externalRadiusPadding + _externalLabelRadius) .innerRadius(_radius - _externalRadiusPadding); var transition = dc.transition(polyline, _chart.transitionDuration(), _chart.transitionDelay()); // this is one rare case where d3.selection differs from d3.transition if (transition.attrTween) { transition .attrTween('points', function (d) { var current = this._current || d; current = {startAngle: current.startAngle, endAngle: current.endAngle}; var interpolate = d3.interpolate(current, d); this._current = interpolate(0); return function (t) { var d2 = interpolate(t); return [arc.centroid(d2), arc2.centroid(d2)]; }; }); } else { transition.attr('points', function (d) { return [arc.centroid(d), arc2.centroid(d)]; }); } transition.style('visibility', function (d) { return d.endAngle - d.startAngle < 0.0001 ? 'hidden' : 'visible'; }); } function updateElements (pieData, arc) { updateSlicePaths(pieData, arc); updateLabels(pieData, arc); updateTitles(pieData); } function updateSlicePaths (pieData, arc) { var slicePaths = _g.selectAll('g.' + _sliceCssClass) .data(pieData) .select('path') .attr('d', function (d, i) { return safeArc(d, i, arc); }); var transition = dc.transition(slicePaths, _chart.transitionDuration(), _chart.transitionDelay()); if (transition.attrTween) { transition.attrTween('d', tweenPie); } transition.attr('fill', fill); } function updateLabels (pieData, arc) { if (_chart.renderLabel()) { var labels = _g.selectAll('text.' + _labelCssClass) .data(pieData); positionLabels(labels, arc); if (_externalLabelRadius && _drawPaths) { updateLabelPaths(pieData, arc); } } } function updateTitles (pieData) { if (_chart.renderTitle()) { _g.selectAll('g.' + _sliceCssClass) .data(pieData) .select('title') .text(function (d) { return _chart.title()(d.data); }); } } function removeElements (slices, labels) { slices.exit().remove(); labels.exit().remove(); } function highlightFilter () { if (_chart.hasFilter()) { _chart.selectAll('g.' + _sliceCssClass).each(function (d) { if (isSelectedSlice(d)) { _chart.highlightSelected(this); } else { _chart.fadeDeselected(this); } }); } else { _chart.selectAll('g.' + _sliceCssClass).each(function () { _chart.resetHighlight(this); }); } } /** * Get or set the external radius padding of the pie chart. This will force the radius of the * pie chart to become smaller or larger depending on the value. * @method externalRadiusPadding * @memberof dc.pieChart * @instance * @param {Number} [externalRadiusPadding=0] * @returns {Number|dc.pieChart} */ _chart.externalRadiusPadding = function (externalRadiusPadding) { if (!arguments.length) { return _externalRadiusPadding; } _externalRadiusPadding = externalRadiusPadding; return _chart; }; /** * Get or set the inner radius of the pie chart. If the inner radius is greater than 0px then the * pie chart will be rendered as a doughnut chart. * @method innerRadius * @memberof dc.pieChart * @instance * @param {Number} [innerRadius=0] * @returns {Number|dc.pieChart} */ _chart.innerRadius = function (innerRadius) { if (!arguments.length) { return _innerRadius; } _innerRadius = innerRadius; return _chart; }; /** * Get or set the outer radius. If the radius is not set, it will be half of the minimum of the * chart width and height. * @method radius * @memberof dc.pieChart * @instance * @param {Number} [radius] * @returns {Number|dc.pieChart} */ _chart.radius = function (radius) { if (!arguments.length) { return _givenRadius; } _givenRadius = radius; return _chart; }; /** * Get or set center x coordinate position. Default is center of svg. * @method cx * @memberof dc.pieChart * @instance * @param {Number} [cx] * @returns {Number|dc.pieChart} */ _chart.cx = function (cx) { if (!arguments.length) { return (_cx || _chart.width() / 2); } _cx = cx; return _chart; }; /** * Get or set center y coordinate position. Default is center of svg. * @method cy * @memberof dc.pieChart * @instance * @param {Number} [cy] * @returns {Number|dc.pieChart} */ _chart.cy = function (cy) { if (!arguments.length) { return (_cy || _chart.height() / 2); } _cy = cy; return _chart; }; function buildArcs () { return d3.arc() .outerRadius(_radius - _externalRadiusPadding) .innerRadius(_innerRadius); } function isSelectedSlice (d) { return _chart.hasFilter(_chart.cappedKeyAccessor(d.data)); } _chart._doRedraw = function () { drawChart(); return _chart; }; /** * Get or set the minimal slice angle for label rendering. Any slice with a smaller angle will not * display a slice label. * @method minAngleForLabel * @memberof dc.pieChart * @instance * @param {Number} [minAngleForLabel=0.5] * @returns {Number|dc.pieChart} */ _chart.minAngleForLabel = function (minAngleForLabel) { if (!arguments.length) { return _minAngleForLabel; } _minAngleForLabel = minAngleForLabel; return _chart; }; function pieLayout () { return d3.pie().sort(null).value(_chart.cappedValueAccessor); } function sliceTooSmall (d) { var angle = (d.endAngle - d.startAngle); return isNaN(angle) || angle < _minAngleForLabel; } function sliceHasNoData (d) { return _chart.cappedValueAccessor(d) === 0; } function tweenPie (b) { b.innerRadius = _innerRadius; var current = this._current; if (isOffCanvas(current)) { current = {startAngle: 0, endAngle: 0}; } else { // only interpolate startAngle & endAngle, not the whole data object current = {startAngle: current.startAngle, endAngle: current.endAngle}; } var i = d3.interpolate(current, b); this._current = i(0); return function (t) { return safeArc(i(t), 0, buildArcs()); }; } function isOffCanvas (current) { return !current || isNaN(current.startAngle) || isNaN(current.endAngle); } function fill (d, i) { return _chart.getColor(d.data, i); } function onClick (d, i) { if (_g.attr('class') !== _emptyCssClass) { _chart.onClick(d.data, i); } } function safeArc (d, i, arc) { var path = arc(d, i); if (path.indexOf('NaN') >= 0) { path = 'M0,0'; } return path; } /** * Title to use for the only slice when there is no data. * @method emptyTitle * @memberof dc.pieChart * @instance * @param {String} [title] * @returns {String|dc.pieChart} */ _chart.emptyTitle = function (title) { if (arguments.length === 0) { return _emptyTitle; } _emptyTitle = title; return _chart; }; /** * Position slice labels offset from the outer edge of the chart. * * The argument specifies the extra radius to be added for slice labels. * @method externalLabels * @memberof dc.pieChart * @instance * @param {Number} [externalLabelRadius] * @returns {Number|dc.pieChart} */ _chart.externalLabels = function (externalLabelRadius) { if (arguments.length === 0) { return _externalLabelRadius; } else if (externalLabelRadius) { _externalLabelRadius = externalLabelRadius; } else { _externalLabelRadius = undefined; } return _chart; }; /** * Get or set whether to draw lines from pie slices to their labels. * * @method drawPaths * @memberof dc.pieChart * @instance * @param {Boolean} [drawPaths] * @returns {Boolean|dc.pieChart} */ _chart.drawPaths = function (drawPaths) { if (arguments.length === 0) { return _drawPaths; } _drawPaths = drawPaths; return _chart; }; function labelPosition (d, arc) { var centroid; if (_externalLabelRadius) { centroid = d3.arc() .outerRadius(_radius - _externalRadiusPadding + _externalLabelRadius) .innerRadius(_radius - _externalRadiusPadding + _externalLabelRadius) .centroid(d); } else { centroid = arc.centroid(d); } if (isNaN(centroid[0]) || isNaN(centroid[1])) { return 'translate(0,0)'; } else { return 'translate(' + centroid + ')'; } } _chart.legendables = function () { return _chart.data().map(function (d, i) { var legendable = {name: d.key, data: d.value, others: d.others, chart: _chart}; legendable.color = _chart.getColor(d, i); return legendable; }); }; _chart.legendHighlight = function (d) { highlightSliceFromLegendable(d, true); }; _chart.legendReset = function (d) { highlightSliceFromLegendable(d, false); }; _chart.legendToggle = function (d) { _chart.onClick({key: d.name, others: d.others}); }; function highlightSliceFromLegendable (legendable, highlighted) { _chart.selectAll('g.pie-slice').each(function (d) { if (legendable.name === d.data.key) { d3.select(this).classed('highlight', highlighted); } }); } return _chart.anchor(parent, chartGroup); };