Source: charts/series-chart.js

import {ascending} from 'd3-array';

import {CompositeChart} from './composite-chart';
import {lineChart} from './line-chart';
import {utils} from '../core/utils';
import {d3compat} from '../core/config';

/**
 * A series chart is a chart that shows multiple series of data overlaid on one chart, where the
 * series is specified in the data. It is a specialization of Composite Chart and inherits all
 * composite features other than recomposing the chart.
 *
 * Examples:
 * - {@link http://dc-js.github.io/dc.js/examples/series.html Series Chart}
 * @mixes CompositeChart
 */
export class SeriesChart extends CompositeChart {
    /**
     * Create a Series Chart.
     * @example
     * // create a series chart under #chart-container1 element using the default global chart group
     * var seriesChart1 = new SeriesChart("#chart-container1");
     * // create a series chart under #chart-container2 element using chart group A
     * var seriesChart2 = new SeriesChart("#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.
     */
    constructor (parent, chartGroup) {
        super(parent, chartGroup);

        this._keySort = (a, b) => ascending(this.keyAccessor()(a), this.keyAccessor()(b));

        this._charts = {};
        this._chartFunction = lineChart;
        this._chartGroup = chartGroup;
        this._seriesAccessor = undefined;
        this._seriesSort = ascending;
        this._valueSort = this._keySort;

        this._mandatoryAttributes().push('seriesAccessor', 'chart');
        this.shareColors(true);
    }

    _compose (subChartArray) {
        super.compose(subChartArray);
    }

    compose (subChartArray) {
        throw new Error('Not supported for this chart type');
    }

    _preprocessData () {
        const keep = [];
        let childrenChanged;

        const nesting = d3compat.nester({
            key: this._seriesAccessor,
            sortKeys: this._seriesSort,
            sortValues: this._valueSort,
            entries: this.data()
        });

        const children =
            nesting.map((sub, i) => {
                const subChart = this._charts[sub.key] || this._chartFunction(this, this._chartGroup , sub.key, i);
                if (!this._charts[sub.key]) {
                    childrenChanged = true;
                }
                this._charts[sub.key] = subChart;
                keep.push(sub.key);
                return subChart
                    .dimension(this.dimension())
                    .group({
                        all: typeof sub.values === 'function' ? sub.values : utils.constant(sub.values)
                    }, sub.key)
                    .keyAccessor(this.keyAccessor())
                    .valueAccessor(this.valueAccessor())
                    .brushOn(false);
            });
        // this works around the fact compositeChart doesn't really
        // have a removal interface
        Object.keys(this._charts)
            .filter(c => keep.indexOf(c) === -1)
            .forEach(c => {
                this._clearChart(c);
                childrenChanged = true;
            });
        this._compose(children);
        if (childrenChanged && this.legend()) {
            this.legend().render();
        }
    }

    _clearChart (c) {
        if (this._charts[c].g()) {
            this._charts[c].g().remove();
        }
        delete this._charts[c];
    }

    _resetChildren () {
        Object.keys(this._charts).map(this._clearChart.bind(this));
        this._charts = {};
    }

    /**
     * Get or set the chart function, which generates the child charts.
     * @example
     * // put curve on the line charts used for the series
     * chart.chart(function(c) { return new LineChart(c).curve(d3.curveBasis); })
     * // do a scatter series chart
     * chart.chart(anchor => new ScatterPlot(anchor))
     * @param {Function} [chartFunction= (anchor) =>  new LineChart(anchor)]
     * @returns {Function|SeriesChart}
     */
    chart (chartFunction) {
        if (!arguments.length) {
            return this._chartFunction;
        }
        this._chartFunction = chartFunction;
        this._resetChildren();
        return this;
    }

    /**
     * **mandatory**
     *
     * Get or set accessor function for the displayed series. Given a datum, this function
     * should return the series that datum belongs to.
     * @example
     * // simple series accessor
     * chart.seriesAccessor(function(d) { return "Expt: " + d.key[0]; })
     * @param {Function} [accessor]
     * @returns {Function|SeriesChart}
     */
    seriesAccessor (accessor) {
        if (!arguments.length) {
            return this._seriesAccessor;
        }
        this._seriesAccessor = accessor;
        this._resetChildren();
        return this;
    }

    /**
     * Get or set a function to sort the list of series by, given series values.
     * @see {@link https://github.com/d3/d3-array/blob/master/README.md#ascending d3.ascending}
     * @see {@link https://github.com/d3/d3-array/blob/master/README.md#descending d3.descending}
     * @example
     * chart.seriesSort(d3.descending);
     * @param {Function} [sortFunction=d3.ascending]
     * @returns {Function|SeriesChart}
     */
    seriesSort (sortFunction) {
        if (!arguments.length) {
            return this._seriesSort;
        }
        this._seriesSort = sortFunction;
        this._resetChildren();
        return this;
    }

    /**
     * Get or set a function to sort each series values by. By default this is the key accessor which,
     * for example, will ensure a lineChart series connects its points in increasing key/x order,
     * rather than haphazardly.
     * @see {@link https://github.com/d3/d3-array/blob/master/README.md#ascending d3.ascending}
     * @see {@link https://github.com/d3/d3-array/blob/master/README.md#descending d3.descending}
     * @example
     * // Default value sort
     * _chart.valueSort(function keySort (a, b) {
     *     return d3.ascending(_chart.keyAccessor()(a), _chart.keyAccessor()(b));
     * });
     * @param {Function} [sortFunction]
     * @returns {Function|SeriesChart}
     */
    valueSort (sortFunction) {
        if (!arguments.length) {
            return this._valueSort;
        }
        this._valueSort = sortFunction;
        this._resetChildren();
        return this;
    }

}

export const seriesChart = (parent, chartGroup) => new SeriesChart(parent, chartGroup);