/** * Stack Mixin is an mixin that provides cross-chart support of stackability using d3.stackD3v3. * @name stackMixin * @memberof dc * @mixin * @param {Object} _chart * @returns {dc.stackMixin} */ dc.stackMixin = function (_chart) { function prepareValues (layer, layerIdx) { var valAccessor = layer.accessor || _chart.valueAccessor(); layer.name = String(layer.name || layerIdx); var allValues = layer.group.all().map(function (d, i) { return { x: _chart.keyAccessor()(d, i), y: layer.hidden ? null : valAccessor(d, i), data: d, layer: layer.name, hidden: layer.hidden }; }); layer.domainValues = allValues.filter(domainFilter()); layer.values = _chart.evadeDomainFilter() ? allValues : layer.domainValues; } var _stackLayout = d3.stack(); var _stack = []; var _titles = {}; var _hidableStacks = false; var _evadeDomainFilter = false; function domainFilter () { if (!_chart.x()) { return dc.utils.constant(true); } var xDomain = _chart.x().domain(); if (_chart.isOrdinal()) { // TODO #416 //var domainSet = d3.set(xDomain); return function () { return true; //domainSet.has(p.x); }; } if (_chart.elasticX()) { return function () { return true; }; } return function (p) { //return true; return p.x >= xDomain[0] && p.x <= xDomain[xDomain.length - 1]; }; } /** * Stack a new crossfilter group onto this chart with an optional custom value accessor. All stacks * in the same chart will share the same key accessor and therefore the same set of keys. * * For example, in a stacked bar chart, the bars of each stack will be positioned using the same set * of keys on the x axis, while stacked vertically. If name is specified then it will be used to * generate the legend label. * @method stack * @memberof dc.stackMixin * @instance * @see {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#group-map-reduce crossfilter.group} * @example * // stack group using default accessor * chart.stack(valueSumGroup) * // stack group using custom accessor * .stack(avgByDayGroup, function(d){return d.value.avgByDay;}); * @param {crossfilter.group} group * @param {String} [name] * @param {Function} [accessor] * @returns {Array<{group: crossfilter.group, name: String, accessor: Function}>|dc.stackMixin} */ _chart.stack = function (group, name, accessor) { if (!arguments.length) { return _stack; } if (arguments.length <= 2) { accessor = name; } var layer = {group: group}; if (typeof name === 'string') { layer.name = name; } if (typeof accessor === 'function') { layer.accessor = accessor; } _stack.push(layer); return _chart; }; dc.override(_chart, 'group', function (g, n, f) { if (!arguments.length) { return _chart._group(); } _stack = []; _titles = {}; _chart.stack(g, n); if (f) { _chart.valueAccessor(f); } return _chart._group(g, n); }); /** * Allow named stacks to be hidden or shown by clicking on legend items. * This does not affect the behavior of hideStack or showStack. * @method hidableStacks * @memberof dc.stackMixin * @instance * @param {Boolean} [hidableStacks=false] * @returns {Boolean|dc.stackMixin} */ _chart.hidableStacks = function (hidableStacks) { if (!arguments.length) { return _hidableStacks; } _hidableStacks = hidableStacks; return _chart; }; function findLayerByName (n) { var i = _stack.map(dc.pluck('name')).indexOf(n); return _stack[i]; } /** * Hide all stacks on the chart with the given name. * The chart must be re-rendered for this change to appear. * @method hideStack * @memberof dc.stackMixin * @instance * @param {String} stackName * @returns {dc.stackMixin} */ _chart.hideStack = function (stackName) { var layer = findLayerByName(stackName); if (layer) { layer.hidden = true; } return _chart; }; /** * Show all stacks on the chart with the given name. * The chart must be re-rendered for this change to appear. * @method showStack * @memberof dc.stackMixin * @instance * @param {String} stackName * @returns {dc.stackMixin} */ _chart.showStack = function (stackName) { var layer = findLayerByName(stackName); if (layer) { layer.hidden = false; } return _chart; }; _chart.getValueAccessorByIndex = function (index) { return _stack[index].accessor || _chart.valueAccessor(); }; _chart.yAxisMin = function () { var min = d3.min(flattenStack(), function (p) { return (p.y < 0) ? (p.y + p.y0) : p.y0; }); return dc.utils.subtract(min, _chart.yAxisPadding()); }; _chart.yAxisMax = function () { var max = d3.max(flattenStack(), function (p) { return (p.y > 0) ? (p.y + p.y0) : p.y0; }); return dc.utils.add(max, _chart.yAxisPadding()); }; function flattenStack () { var valueses = _chart.data().map(function (layer) { return layer.domainValues; }); return Array.prototype.concat.apply([], valueses); } _chart.xAxisMin = function () { var min = d3.min(flattenStack(), dc.pluck('x')); return dc.utils.subtract(min, _chart.xAxisPadding(), _chart.xAxisPaddingUnit()); }; _chart.xAxisMax = function () { var max = d3.max(flattenStack(), dc.pluck('x')); return dc.utils.add(max, _chart.xAxisPadding(), _chart.xAxisPaddingUnit()); }; /** * Set or get the title function. Chart class will use this function to render svg title (usually interpreted by * browser as tooltips) for each child element in the chart, i.e. a slice in a pie chart or a bubble in a bubble chart. * Almost every chart supports title function however in grid coordinate chart you need to turn off brush in order to * use title otherwise the brush layer will block tooltip trigger. * * If the first argument is a stack name, the title function will get or set the title for that stack. If stackName * is not provided, the first stack is implied. * @method title * @memberof dc.stackMixin * @instance * @example * // set a title function on 'first stack' * chart.title('first stack', function(d) { return d.key + ': ' + d.value; }); * // get a title function from 'second stack' * var secondTitleFunction = chart.title('second stack'); * @param {String} [stackName] * @param {Function} [titleAccessor] * @returns {String|dc.stackMixin} */ dc.override(_chart, 'title', function (stackName, titleAccessor) { if (!stackName) { return _chart._title(); } if (typeof stackName === 'function') { return _chart._title(stackName); } if (stackName === _chart._groupName && typeof titleAccessor === 'function') { return _chart._title(titleAccessor); } if (typeof titleAccessor !== 'function') { return _titles[stackName] || _chart._title(); } _titles[stackName] = titleAccessor; return _chart; }); /** * Gets or sets the stack layout algorithm, which computes a baseline for each stack and * propagates it to the next. * @method stackLayout * @memberof dc.stackMixin * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Stack-Layout.md d3.stackD3v3} * @param {Function} [stack=d3.stackD3v3] * @returns {Function|dc.stackMixin} */ _chart.stackLayout = function (stack) { if (!arguments.length) { return _stackLayout; } _stackLayout = stack; return _chart; }; /** * Since dc.js 2.0, there has been {@link https://github.com/dc-js/dc.js/issues/949 an issue} * where points are filtered to the current domain. While this is a useful optimization, it is * incorrectly implemented: the next point outside the domain is required in order to draw lines * that are clipped to the bounds, as well as bars that are partly clipped. * * A fix will be included in dc.js 2.1.x, but a workaround is needed for dc.js 2.0 and until * that fix is published, so set this flag to skip any filtering of points. * * Once the bug is fixed, this flag will have no effect, and it will be deprecated. * @method evadeDomainFilter * @memberof dc.stackMixin * @instance * @param {Boolean} [evadeDomainFilter=false] * @returns {Boolean|dc.stackMixin} */ _chart.evadeDomainFilter = function (evadeDomainFilter) { if (!arguments.length) { return _evadeDomainFilter; } _evadeDomainFilter = evadeDomainFilter; return _chart; }; function visibility (l) { return !l.hidden; } _chart.data(function () { var layers = _stack.filter(visibility); if (!layers.length) { return []; } layers.forEach(prepareValues); var v4data = layers[0].values.map(function (v, i) { var col = {x: v.x}; layers.forEach(function (layer) { col[layer.name] = layer.values[i].y; }); return col; }); var keys = layers.map(function (layer) { return layer.name; }); var v4result = _chart.stackLayout().keys(keys)(v4data); v4result.forEach(function (series, i) { series.forEach(function (ys, j) { layers[i].values[j].y0 = ys[0]; layers[i].values[j].y1 = ys[1]; }); }); return layers; }); _chart._ordinalXDomain = function () { var flat = flattenStack().map(dc.pluck('data')); var ordered = _chart._computeOrderedGroups(flat); return ordered.map(_chart.keyAccessor()); }; _chart.colorAccessor(function (d) { var layer = this.layer || this.name || d.name || d.layer; return layer; }); _chart.legendables = function () { return _stack.map(function (layer, i) { return { chart: _chart, name: layer.name, hidden: layer.hidden || false, color: _chart.getColor.call(layer, layer.values, i) }; }); }; _chart.isLegendableHidden = function (d) { var layer = findLayerByName(d.name); return layer ? layer.hidden : false; }; _chart.legendToggle = function (d) { if (_hidableStacks) { if (_chart.isLegendableHidden(d)) { _chart.showStack(d.name); } else { _chart.hideStack(d.name); } //_chart.redraw(); _chart.renderGroup(); } }; return _chart; };