import {stack} from 'd3-shape';
import {max, min} from 'd3-array';
import {pluck, utils} from '../core/utils';
import {CoordinateGridMixin} from './coordinate-grid-mixin';
/**
* Stack Mixin is an mixin that provides cross-chart support of stackability using d3.stack.
* @mixin StackMixin
* @mixes CoordinateGridMixin
*/
export class StackMixin extends CoordinateGridMixin {
constructor () {
super();
this._stackLayout = stack();
this._stack = [];
this._titles = {};
this._hidableStacks = false;
this._evadeDomainFilter = false;
this.data(() => {
const layers = this._stack.filter(this._visibility);
if (!layers.length) {
return [];
}
layers.forEach((l, i) => this._prepareValues(l, i));
const v4data = layers[0].values.map((v, i) => {
const col = {x: v.x};
layers.forEach(layer => {
col[layer.name] = layer.values[i].y;
});
return col;
});
const keys = layers.map(layer => layer.name);
const v4result = this.stackLayout().keys(keys)(v4data);
v4result.forEach((series, i) => {
series.forEach((ys, j) => {
layers[i].values[j].y0 = ys[0];
layers[i].values[j].y1 = ys[1];
});
});
return layers;
});
this.colorAccessor(function (d) {
return this.layer || this.name || d.name || d.layer;
});
}
_prepareValues (layer, layerIdx) {
const valAccessor = layer.accessor || this.valueAccessor();
layer.name = String(layer.name || layerIdx);
const allValues = layer.group.all().map((d, i) => ({
x: this.keyAccessor()(d, i),
y: layer.hidden ? null : valAccessor(d, i),
data: d,
layer: layer.name,
hidden: layer.hidden
}));
layer.domainValues = allValues.filter(l => this._domainFilter()(l));
layer.values = this.evadeDomainFilter() ? allValues : layer.domainValues;
}
_domainFilter () {
if (!this.x()) {
return utils.constant(true);
}
const xDomain = this.x().domain();
if (this.isOrdinal()) {
// TODO #416
//var domainSet = d3.set(xDomain);
return () => true //domainSet.has(p.x);
;
}
if (this.elasticX()) {
return () => true;
}
return p => 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.
* @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}>|StackMixin}
*/
stack (group, name, accessor) {
if (!arguments.length) {
return this._stack;
}
if (arguments.length <= 2) {
accessor = name;
}
const layer = {group: group};
if (typeof name === 'string') {
layer.name = name;
}
if (typeof accessor === 'function') {
layer.accessor = accessor;
}
this._stack.push(layer);
return this;
}
group (g, n, f) {
if (!arguments.length) {
return super.group();
}
this._stack = [];
this._titles = {};
this.stack(g, n);
if (f) {
this.valueAccessor(f);
}
return super.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.
* @param {Boolean} [hidableStacks=false]
* @returns {Boolean|StackMixin}
*/
hidableStacks (hidableStacks) {
if (!arguments.length) {
return this._hidableStacks;
}
this._hidableStacks = hidableStacks;
return this;
}
_findLayerByName (n) {
const i = this._stack.map(pluck('name')).indexOf(n);
return this._stack[i];
}
/**
* Hide all stacks on the chart with the given name.
* The chart must be re-rendered for this change to appear.
* @param {String} stackName
* @returns {StackMixin}
*/
hideStack (stackName) {
const layer = this._findLayerByName(stackName);
if (layer) {
layer.hidden = true;
}
return this;
}
/**
* Show all stacks on the chart with the given name.
* The chart must be re-rendered for this change to appear.
* @param {String} stackName
* @returns {StackMixin}
*/
showStack (stackName) {
const layer = this._findLayerByName(stackName);
if (layer) {
layer.hidden = false;
}
return this;
}
getValueAccessorByIndex (index) {
return this._stack[index].accessor || this.valueAccessor();
}
yAxisMin () {
const m = min(this._flattenStack(), p => (p.y < 0) ? (p.y + p.y0) : p.y0);
return utils.subtract(m, this.yAxisPadding());
}
yAxisMax () {
const m = max(this._flattenStack(), p => (p.y > 0) ? (p.y + p.y0) : p.y0);
return utils.add(m, this.yAxisPadding());
}
_flattenStack () {
// A round about way to achieve flatMap
// When target browsers support flatMap, just replace map -> flatMap, no concat needed
const values = this.data().map(layer => layer.domainValues);
return [].concat(...values);
}
xAxisMin () {
const m = min(this._flattenStack(), pluck('x'));
return utils.subtract(m, this.xAxisPadding(), this.xAxisPaddingUnit());
}
xAxisMax () {
const m = max(this._flattenStack(), pluck('x'));
return utils.add(m, this.xAxisPadding(), this.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.
* @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|StackMixin}
*/
title (stackName, titleAccessor) {
if (!stackName) {
return super.title();
}
if (typeof stackName === 'function') {
return super.title(stackName);
}
if (stackName === this._groupName && typeof titleAccessor === 'function') {
return super.title(titleAccessor);
}
if (typeof titleAccessor !== 'function') {
return this._titles[stackName] || super.title();
}
this._titles[stackName] = titleAccessor;
return this;
}
/**
* Gets or sets the stack layout algorithm, which computes a baseline for each stack and
* propagates it to the next.
* @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|StackMixin}
*/
stackLayout (_stack) {
if (!arguments.length) {
return this._stackLayout;
}
this._stackLayout = _stack;
return this;
}
/**
* 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.
* @param {Boolean} [evadeDomainFilter=false]
* @returns {Boolean|StackMixin}
*/
evadeDomainFilter (evadeDomainFilter) {
if (!arguments.length) {
return this._evadeDomainFilter;
}
this._evadeDomainFilter = evadeDomainFilter;
return this;
}
_visibility (l) {
return !l.hidden;
}
_ordinalXDomain () {
const flat = this._flattenStack().map(pluck('data'));
const ordered = this._computeOrderedGroups(flat);
return ordered.map(this.keyAccessor());
}
legendables () {
return this._stack.map((layer, i) => ({
chart: this,
name: layer.name,
hidden: layer.hidden || false,
color: this.getColor.call(layer, layer.values, i)
}));
}
isLegendableHidden (d) {
const layer = this._findLayerByName(d.name);
return layer ? layer.hidden : false;
}
legendToggle (d) {
if (this._hidableStacks) {
if (this.isLegendableHidden(d)) {
this.showStack(d.name);
} else {
this.hideStack(d.name);
}
//_chart.redraw();
this.renderGroup();
}
}
}