dc.js Source: sunburst-chart.js (original) (raw)
/**
The sunburst chart implementation is usually used to visualize a small tree distribution. The sunburst
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.
The keys used in the sunburst chart should be arrays, representing paths in the tree.
When filtering, the sunburst chart creates instances of {@link dc.filters.HierarchyFilter HierarchyFilter}.
@class sunburstChart
@memberof dc
@mixes dc.capMixin
@mixes dc.colorMixin
@mixes dc.baseMixin
@example
// create a sunburst chart under #chart-container1 element using the default global chart group
var chart1 = dc.sunburstChart('#chart-container1');
// create a sunburst chart under #chart-container2 element using chart group A
var chart2 = dc.sunburstChart('#chart-container2', 'chartGroupA');
@param {String|node|d3.selection} parent - Any valid
{@link https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#selecting-elements 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.sunburstChart} **/ dc.sunburstChart = function (parent, chartGroup) { var DEFAULT_MIN_ANGLE_FOR_LABEL = 0.5;
var _sliceCssClass = 'pie-slice'; var _emptyCssClass = 'empty-chart'; var _emptyTitle = 'empty';
var _radius, _givenRadius, // given radius, if any _innerRadius = 0, _ringSizes; var _g; var _cx; var _cy; var _minAngleForLabel = DEFAULT_MIN_ANGLE_FOR_LABEL; var _externalLabelRadius; var _chart = dc.capMixin(dc.colorMixin(dc.baseMixin({}))); _chart.colorAccessor(_chart.cappedKeyAccessor);
// override cap mixin _chart.ordering(dc.pluck('key'));
// Handle cases if value corresponds to generated parent nodes function extendedValueAccessor (d) { if (d.path) { return d.value; } return _chart.cappedValueAccessor(d); }
function scaleRadius (ringIndex, y) { if (ringIndex === 0) { return _innerRadius; } else { var customRelativeRadius = d3.sum(_chart.ringSizes().relativeRingSizes.slice(0, ringIndex)); var scaleFactor = (ringIndex * (1 / _chart.ringSizes().relativeRingSizes.length)) / customRelativeRadius; var standardRadius = (y - _chart.ringSizes().rootOffset) / (1 - _chart.ringSizes().rootOffset) * (_radius - _innerRadius); return _innerRadius + standardRadius / scaleFactor; } }
_chart.title(function (d) { return _chart.cappedKeyAccessor(d) + ': ' + extendedValueAccessor(d); });
_chart.label(_chart.cappedKeyAccessor); _chart.renderLabel(true);
_chart.transitionDuration(350);
_chart.filterHandler(function (dimension, filters) { if (filters.length === 0) { dimension.filter(null); } else { dimension.filterFunction(function (d) { for (var i = 0; i < filters.length; i++) { var filter = filters[i]; if (filter.isFiltered && filter.isFiltered(d)) { return true; } } return false; }); } return filters; });
_chart._doRender = function () { _chart.resetSvg(); _g = _chart.svg() .append('g') .attr('transform', 'translate(' + _chart.cx() + ',' + _chart.cy() + ')'); 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 partitionedNodes, cdata; // if we have data... if (d3.sum(_chart.data(), _chart.valueAccessor())) { cdata = dc.utils.toHierarchy(_chart.data(), _chart.valueAccessor()); partitionedNodes = partitionNodes(cdata); // First one is the root, which is not needed partitionedNodes.nodes.shift(); _g.classed(_emptyCssClass, false); } else { // otherwise we'd be getting NaNs, so override // note: abuse others for its ignoring the value accessor cdata = dc.utils.toHierarchy([], function (d) { return d.value; }); partitionedNodes = partitionNodes(cdata); _g.classed(_emptyCssClass, true); } _chart.ringSizes().rootOffset = partitionedNodes.rootOffset; _chart.ringSizes().relativeRingSizes = partitionedNodes.relativeRingSizes; if (_g) { var slices = _g.selectAll('g.' + _sliceCssClass) .data(partitionedNodes.nodes); createElements(slices, arc, partitionedNodes.nodes); updateElements(partitionedNodes.nodes, arc); removeElements(slices); highlightFilter(); dc.transition(_g, _chart.transitionDuration(), _chart.transitionDelay()) .attr('transform', 'translate(' + _chart.cx() + ',' + _chart.cy() + ')'); } }
function createElements (slices, arc, sunburstData) { var slicesEnter = createSliceNodes(slices); createSlicePath(slicesEnter, arc); createTitles(slicesEnter); createLabels(sunburstData, arc); }
function createSliceNodes (slices) { var slicesEnter = slices .enter() .append('g') .attr('class', function (d, i) { return _sliceCssClass + ' _' + i + ' ' + _sliceCssClass + '-level-' + d.depth; }); return slicesEnter; }
function createSlicePath (slicesEnter, arc) { var slicePath = slicesEnter.append('path') .attr('fill', fill) .on('click', onClick) .attr('d', function (d) { return safeArc(arc, d); }); var transition = dc.transition(slicePath, _chart.transitionDuration()); if (transition.attrTween) { transition.attrTween('d', tweenSlice); } }
function createTitles (slicesEnter) { if (_chart.renderTitle()) { slicesEnter.append('title').text(function (d) { return _chart.title()(d); }); } }
function positionLabels (labelsEnter, arc) { dc.transition(labelsEnter, _chart.transitionDuration()) .attr('transform', function (d) { return labelPosition(d, arc); }) .attr('text-anchor', 'middle') .text(function (d) { // position label... if (sliceHasNoData(d) || sliceTooSmall(d)) { return ''; } return _chart.label()(d); }); }
function createLabels (sunburstData, arc) { if (_chart.renderLabel()) { var labels = _g.selectAll('text.' + _sliceCssClass) .data(sunburstData); labels.exit().remove(); var labelsEnter = labels .enter() .append('text') .attr('class', function (d, i) { var classes = _sliceCssClass + ' _' + i; if (_externalLabelRadius) { classes += ' external'; } return classes; }) .on('click', onClick); positionLabels(labelsEnter, arc); } }
function updateElements (sunburstData, arc) { updateSlicePaths(sunburstData, arc); updateLabels(sunburstData, arc); updateTitles(sunburstData); }
function updateSlicePaths (sunburstData, arc) { var slicePaths = _g.selectAll('g.' + _sliceCssClass) .data(sunburstData) .select('path') .attr('d', function (d, i) { return safeArc(arc, d); }); var transition = dc.transition(slicePaths, _chart.transitionDuration()); if (transition.attrTween) { transition.attrTween('d', tweenSlice); } transition.attr('fill', fill); }
function updateLabels (sunburstData, arc) { if (_chart.renderLabel()) { var labels = _g.selectAll('text.' + _sliceCssClass) .data(sunburstData); positionLabels(labels, arc); } }
function updateTitles (sunburstData) { if (_chart.renderTitle()) { _g.selectAll('g.' + _sliceCssClass) .data(sunburstData) .select('title') .text(function (d) { return _chart.title()(d); }); } }
function removeElements (slices) { slices.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 (d) { _chart.resetHighlight(this); }); } }
/**
- Get or set the inner radius of the sunburst chart. If the inner radius is greater than 0px then the
- sunburst chart will be rendered as a doughnut chart. Default inner radius is 0px.
- @method innerRadius
- @memberof dc.sunburstChart
- @instance
- @param {Number} [innerRadius=0]
- @returns {Number|dc.sunburstChart} */ _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.sunburstChart
- @instance
- @param {Number} [radius]
- @returns {Number|dc.sunburstChart} */ _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.sunburstChart
- @instance
- @param {Number} [cx]
- @returns {Number|dc.sunburstChart} */ _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.sunburstChart
- @instance
- @param {Number} [cy]
- @returns {Number|dc.sunburstChart} */ _chart.cy = function (cy) { if (!arguments.length) { return (_cy || _chart.height() / 2); } _cy = cy; 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.sunburstChart
- @instance
- @param {Number} [minAngleForLabel=0.5]
- @returns {Number|dc.sunburstChart} */ _chart.minAngleForLabel = function (minAngleForLabel) { if (!arguments.length) { return _minAngleForLabel; } _minAngleForLabel = minAngleForLabel; return _chart; };
/**
- Title to use for the only slice when there is no data.
- @method emptyTitle
- @memberof dc.sunburstChart
- @instance
- @param {String} [title]
- @returns {String|dc.sunburstChart} */ _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.sunburstChart
@instance
@param {Number} [externalLabelRadius]
@returns {Number|dc.sunburstChart} */ _chart.externalLabels = function (externalLabelRadius) { if (arguments.length === 0) { return _externalLabelRadius; } else if (externalLabelRadius) { _externalLabelRadius = externalLabelRadius; } else { _externalLabelRadius = undefined; }
return _chart;
};
/**
- Constructs the default RingSizes parameter for {@link dc.sunburstChart#ringSizes ringSizes()},
- which makes the rings narrower as they get farther away from the center.
- Can be used as a parameter to ringSizes() to reset the default behavior, or modified for custom ring sizes.
- @method defaultRingSizes
- @memberof dc.sunburstChart
- @instance
- @example
- var chart = new dc.sunburstChart(...);
- chart.ringSizes(chart.defaultRingSizes())
- @returns {RingSizes} */ _chart.defaultRingSizes = function () { return { partitionDy: function () { return _radius * _radius; }, scaleInnerRadius: function (d) { return d.data.path && d.data.path.length === 1 ? _innerRadius : Math.sqrt(d.y0); }, scaleOuterRadius: function (d) { return Math.sqrt(d.y1); }, relativeRingSizesFunction: function () {return [];} }; };
/**
- Constructs a RingSizes parameter for {@link dc.sunburstChart#ringSizes ringSizes()}
- that will make the chart rings equally wide.
- @method equalRingSizes
- @memberof dc.sunburstChart
- @instance
- @example
- var chart = new dc.sunburstChart(...);
- chart.ringSizes(chart.equalRingSizes())
- @returns {RingSizes} */ _chart.equalRingSizes = function () { return _chart.relativeRingSizes( function (ringCount) { var i; var result = []; for (i = 0; i < ringCount; i++) { result.push(1 / ringCount); } return result; } ); };
/**
- Constructs a RingSizes parameter for {@link dc.sunburstChart#ringSizes ringSizes()} using the given function
- to determine each rings width.
- The function must return an array containing portion values for each ring/level of the chart.
- The length of the array must match the number of rings of the chart at runtime, which is provided as the only
- argument.
- The sum of all portions from the array must be 1 (100%).
- @example
- // specific relative portions (the number of rings (3) is known in this case)
- chart.ringSizes(chart.relativeRingSizes(function (ringCount) {
return [.1, .3, .6];
- });
- @method relativeRingSizes
- @memberof dc.sunburstChart
- @instance
- @param {Function} [relativeRingSizesFunction]
- @returns {RingSizes} */ _chart.relativeRingSizes = function (relativeRingSizesFunction) { function assertPortionsArray (relativeSizes, numberOfRings) { if (!Array.isArray(relativeSizes)) { throw new dc.errors.BadArgumentException('relativeRingSizes function must return an array'); } var portionsSum = d3.sum(relativeSizes); if (Math.abs(portionsSum - 1) > dc.constants.NEGLIGIBLE_NUMBER) { throw new dc.errors.BadArgumentException( 'relativeRingSizes : portions must add up to 1, but sum was ' + portionsSum); } if (relativeSizes.length !== numberOfRings) { throw new dc.errors.BadArgumentException( 'relativeRingSizes : number of values must match number of rings (' + numberOfRings + ') but was ' + relativeSizes.length); } } return { partitionDy: function () { return 1; }, scaleInnerRadius: function (d) { return scaleRadius(d.data.path.length - 1, d.y0); }, scaleOuterRadius: function (d) { return scaleRadius(d.data.path.length, d.y1); }, relativeRingSizesFunction: function (ringCount) { var result = relativeRingSizesFunction(ringCount); assertPortionsArray(result, ringCount); return result; } };
};
/**
- Get or set the strategy to use for sizing the charts rings.
- There are three strategies available
- {@link dc.sunburstChart#defaultRingSizes
defaultRingSizes
}: the rings get narrower farther away from the center
- {@link dc.sunburstChart#defaultRingSizes
- {@link dc.sunburstChart#relativeRingSizes
relativeRingSizes
}: set the ring sizes as portions of 1
- {@link dc.sunburstChart#relativeRingSizes
- {@link dc.sunburstChart#equalRingSizes
equalRingSizes
}: the rings are equally wide
- {@link dc.sunburstChart#equalRingSizes
- You can modify the returned strategy, or create your own, for custom ring sizing.
- RingSizes is a duck-typed interface that must support the following methods:
partitionDy()
: used for
- {@link https://github.com/d3/d3-hierarchy/blob/v1.1.9/README.md#partition_size
d3.partition.size
} scaleInnerRadius(d)
: takes datum and returns radius for
- {@link https://github.com/d3/d3-shape/blob/v1.3.7/README.md#arc_innerRadius
d3.arc.innerRadius
} scaleOuterRadius(d)
: takes datum and returns radius for
- {@link https://github.com/d3/d3-shape/blob/v1.3.7/README.md#arc_outerRadius
d3.arc.outerRadius
} relativeRingSizesFunction(ringCount)
: takes ring count and returns an array of portions that
- must add up to 1
- @example
- // make rings equally wide
- chart.ringSizes(chart.equalRingSizes())
- // reset to default behavior
- chart.ringSizes(chart.defaultRingSizes()))
- @method ringSizes
- @memberof dc.sunburstChart
- @instance
- @param {RingSizes} ringSizes
- @returns {Object|dc.sunburstChart} */ _chart.ringSizes = function (ringSizes) { if (!arguments.length) { if (!_ringSizes) { _ringSizes = this.defaultRingSizes(); } return _ringSizes; } _ringSizes = ringSizes; return _chart; };
function buildArcs () { return d3.arc() .startAngle(function (d) { return d.x0; }) .endAngle(function (d) { return d.x1; }) .innerRadius(function (d) { return _chart.ringSizes().scaleInnerRadius(d); }) .outerRadius(function (d) { return _chart.ringSizes().scaleOuterRadius(d); }); }
function isSelectedSlice (d) { return isPathFiltered(d.path); }
function isPathFiltered (path) { for (var i = 0; i < _chart.filters().length; i++) { var currentFilter = _chart.filters()[i]; if (currentFilter.isFiltered(path)) { return true; } } return false; }
// returns all filters that are a parent or child of the path function filtersForPath (path) { var pathFilter = dc.filters.HierarchyFilter(path); var filters = []; for (var i = 0; i < _chart.filters().length; i++) { var currentFilter = _chart.filters()[i]; if (currentFilter.isFiltered(path) || pathFilter.isFiltered(currentFilter)) { filters.push(currentFilter); } } return filters; }
_chart._doRedraw = function () { drawChart(); return _chart; };
function partitionNodes (data) { var getSortable = function (d) { return {'key': d.data.key, 'value': d.value}; }; // The changes picked up from https://github.com/d3/d3-hierarchy/issues/50 var hierarchy = d3.hierarchy(data) .sum(function (d) { return d.children ? 0 : extendedValueAccessor(d); }) .sort(function (a, b) { return d3.ascending(_chart.ordering()(getSortable(a)), _chart.ordering()(getSortable(b))); });
var partition = d3.partition().size([2 * Math.PI, _chart.ringSizes().partitionDy()]); partition(hierarchy); // In D3v4 the returned data is slightly different, change it enough to suit our purposes. var nodes = hierarchy.descendants().map(function (d) { d.key = d.data.key; d.path = d.data.path; return d; }); var relativeSizes = _chart.ringSizes().relativeRingSizesFunction(hierarchy.height); return { nodes: nodes, rootOffset: hierarchy.y1, relativeRingSizes: relativeSizes };
}
function sliceTooSmall (d) { var angle = d.x1 - d.x0; return isNaN(angle) || angle < _minAngleForLabel; }
function sliceHasNoData (d) { return extendedValueAccessor(d) === 0; }
function tweenSlice (d) { var current = this._current; if (isOffCanvas(current)) { current = {x0: 0, x1: 0, y0: 0, y1: 0}; } var tweenTarget = { x0: d.x0, x1: d.x1, y0: d.y0, y1: d.y1 }; var i = d3.interpolate(current, tweenTarget); this._current = i(0); return function (t) { return safeArc(buildArcs(), Object.assign({}, d, i(t))); }; }
function isOffCanvas (d) { return !d || isNaN(d.x0) || isNaN(d.y0); }
function fill (d, i) { return _chart.getColor(d.data, i); }
function _onClick (d) { // Clicking on Legends do not filter, it throws exception // Must be better way to handle this, in legends we need to access
d.key
var path = d.path || d.key; var filter = dc.filters.HierarchyFilter(path);// filters are equal to, parents or children of the path. var filters = filtersForPath(path); var exactMatch = false; // clear out any filters that cover the path filtered. for (var i = filters.length - 1; i >= 0; i--) { var currentFilter = filters[i]; if (dc.utils.arraysIdentical(currentFilter, path)) { exactMatch = true; } _chart.filter(filters[i]); } dc.events.trigger(function () { // if it is a new filter - put it in. if (!exactMatch) { _chart.filter(filter); } _chart.redrawGroup(); });
}
_chart.onClick = onClick;
function onClick (d, i) { if (_g.attr('class') !== _emptyCssClass) { _onClick(d, i); } }
function safeArc (arc, d) { var path = arc(d); if (path.indexOf('NaN') >= 0) { path = 'M0,0'; } return path; }
function labelPosition (d, arc) { var centroid; if (_externalLabelRadius) { centroid = d3.svg.arc() .outerRadius(_radius + _externalLabelRadius) .innerRadius(_radius + _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.key) { d3.select(this).classed('highlight', highlighted); } }); }
return _chart.anchor(parent, chartGroup);
};