controls/Line/Line.js

"use strict";
import { GraphContent } from "../../core";
import { getDefaultValue } from "../../core/BaseConfig";
import constants from "../../helpers/constants";
import {
    prepareLabelShapeItem,
    removeLabelShapeItem
} from "../../helpers/label";
import { removeLegendItem, reflowLegend } from "../../helpers/legend";
import {
    createRegion,
    hideAllRegions,
    removeRegion,
    translateRegion,
    areRegionsIdentical,
    createValueRegion,
    isSingleTargetDisplayed
} from "../../helpers/region";
import styles from "../../helpers/styles";
import utils from "../../helpers/utils";
import {
    clear,
    clickHandler,
    draw,
    getDataPointValues,
    drawDataPoints,
    drawDataLines,
    hoverHandler,
    prepareLegendItems,
    processDataPoints,
    translateLineGraph
} from "./helpers/helpers";
import LineConfig from "./LineConfig";

/**
 * @typedef {object} Line
 * @typedef {object} GraphContent
 * @typedef {object} LineConfig
 */
/**
 * Calculates the min and max values for Y Axis or Y2 Axis.
 * First we filter out values that are `null`, this is a result of
 * datapoint being part of being in a non-contiguous series and then we
 * get the min and max values for the Y or Y2 axis domain.
 *
 * @private
 * @param {Array} values - Datapoint values
 * @param {string} axis - y or y2
 * @returns {object} - Contains min and max values for the data points for Y and Y2 axis
 */
const calculateValuesRange = (values, axis = constants.Y_AXIS) => {
    const yAxisValuesList = values.filter((i) => i.y !== null).map((i) => i.y);
    return {
        [axis]: {
            min: Math.min(...yAxisValuesList),
            max: Math.max(...yAxisValuesList)
        }
    };
};

/**
 * Data point sets can be loaded using this function.
 * Load function validates, clones and stores the input onto a config object.
 *
 * @private
 * @param {object} inputJSON - Input JSON provided by the consumer
 * @returns {object} LineConfig config object containing consumer data
 */
const loadInput = (inputJSON) =>
    new LineConfig().setInput(inputJSON).validateInput().clone().getConfig();

/**
 * A Line graph is a graph used to represent a collection of data
 * points connected by a line along the X and Y Axis.
 *
 * Lifecycle functions include:
 *  * Load
 *  * Generate
 *  * Unload
 *  * Destroy
 *
 * @module Line
 * @class Line
 */
class Line extends GraphContent {
    /**
     * @class
     * @param {LineConfig} input - Input JSON instance created using GraphConfig
     */
    constructor(input) {
        super();
        this.config = loadInput(input);
        this.type = "Line";
        this.config.yAxis = getDefaultValue(
            this.config.yAxis,
            constants.Y_AXIS
        );
        this.valuesRange = calculateValuesRange(
            this.config.values,
            this.config.yAxis
        );
        this.dataTarget = {};
    }

    /**
     * @inheritdoc
     */
    load(graph) {
        this.dataTarget = processDataPoints(graph.config, this.config);
        draw(graph.scale, graph.config, graph.svg, this.dataTarget);
        if (!utils.isEmptyArray(this.dataTarget.values)) {
            if (!utils.isEmptyArray(this.dataTarget.valueRegionSubset)) {
                createValueRegion(
                    graph.scale,
                    graph.config,
                    graph.svg.select(`.${styles.regionGroup}`),
                    this.dataTarget.valueRegionSubset,
                    `region_${this.dataTarget.key}`,
                    this.config.yAxis,
                    this.dataTarget.interpolationType
                );
            } else if (utils.notEmpty(this.dataTarget.regions)) {
                createRegion(
                    graph.scale,
                    graph.config,
                    graph.svg.select(`.${styles.regionGroup}`),
                    this.dataTarget.regions,
                    `region_${this.dataTarget.key}`,
                    this.config.yAxis
                );
            }
        }

        prepareLegendItems(
            graph.config,
            {
                clickHandler: clickHandler(
                    graph,
                    this,
                    graph.config,
                    graph.svg
                ),
                hoverHandler: hoverHandler(graph.config.shownTargets, graph.svg)
            },
            this.dataTarget,
            graph.legendSVG
        );
        prepareLabelShapeItem(
            graph.config,
            this.dataTarget,
            graph.axesLabelShapeGroup[this.config.yAxis]
        );
        return this;
    }

    /**
     * @inheritdoc
     */
    unload(graph) {
        clear(graph.svg, this.dataTarget);
        removeRegion(
            graph.svg.select(`.${styles.regionGroup}`),
            this.dataTarget
        );
        removeLegendItem(graph.legendSVG, this.dataTarget);
        removeLabelShapeItem(
            graph.axesLabelShapeGroup[this.config.yAxis],
            this.dataTarget,
            graph.config
        );
        this.dataTarget = {};
        this.config = {};
        return this;
    }

    /**
     * @inheritdoc
     */
    resize(graph) {
        if (
            !utils.isEmptyArray(this.dataTarget.values) &&
            graph.config.shownTargets.indexOf(this.dataTarget.key) > -1
        ) {
            if (
                utils.notEmpty(this.dataTarget.regions) ||
                !utils.isEmptyArray(this.dataTarget.valueRegionSubset)
            ) {
                if (
                    isSingleTargetDisplayed(
                        graph.config.shownTargets,
                        graph.content
                    )
                ) {
                    graph.config.shouldHideAllRegion = false;
                } else {
                    graph.config.shouldHideAllRegion = !areRegionsIdentical(
                        graph.svg
                    );
                }
            } else {
                graph.config.shouldHideAllRegion = true;
            }
        }
        if (graph.config.shouldHideAllRegion) {
            hideAllRegions(graph.svg);
        }
        translateRegion(
            graph.scale,
            graph.config,
            graph.svg.select(
                `.${styles.regionGroup}`,
                this.dataTarget.valueRegionSubset
            ),
            this.dataTarget.yAxis,
            !utils.isEmptyArray(this.dataTarget.valueRegionSubset),
            this.dataTarget.interpolationType
        );
        translateLineGraph(graph.scale, graph.svg, graph.config);
        return this;
    }

    /**
     * @inheritdoc
     */
    reflow(graph, graphData) {
        this.config.values = graphData.values;
        this.dataTarget = processDataPoints(graph.config, this.config);
        const eventHandlers = {
            clickHandler: clickHandler(graph, this, graph.config, graph.svg),
            hoverHandler: hoverHandler(graph.config.shownTargets, graph.svg)
        };
        const position = graph.config.shownTargets.lastIndexOf(graphData.key);
        if (position > -1) {
            graph.config.shownTargets.splice(position, 1);
        }
        reflowLegend(
            graph.legendSVG,
            graph.content.filter((line) => line.config.key === graphData.key)[0]
                .config,
            graph,
            eventHandlers
        );
        const lineSVG = graph.svg
            .select(`g[aria-describedby="${graphData.key}"]`)
            .selectAll(`.${styles.line}`)
            .data([this.dataTarget]);
        drawDataLines(graph.scale, graph.config, lineSVG.enter());
        lineSVG.exit().remove();

        if (utils.isEmpty(graphData.values)) {
            removeLabelShapeItem(
                graph.axesLabelShapeGroup[this.config.yAxis],
                this.dataTarget,
                graph.config
            );
        }

        if (graph.config.showShapes) {
            const currentPointsPath = graph.svg
                .select(`g[aria-describedby="${graphData.key}"]`)
                .selectAll(`.${styles.pointGroup}`)
                .data(this.dataTarget);
            currentPointsPath.exit().remove();
            const pointPath = graph.svg
                .select(`g[aria-describedby="${graphData.key}"]`)
                .select(`.${styles.currentPointsGroup}`)
                .selectAll(`[class*="${styles.point}"]`)
                .data(getDataPointValues(this.dataTarget));
            drawDataPoints(graph.scale, graph.config, pointPath.enter());
            pointPath
                .exit()
                .transition()
                .call(
                    constants.d3Transition(
                        graph.config.settingsDictionary.transition
                    )
                )
                .remove();
        }
        this.valuesRange = calculateValuesRange(
            this.config.values,
            this.config.yAxis
        );
    }

    /**
     * @inheritdoc
     */
    redraw(graph) {
        clear(graph.svg, this.dataTarget);
        draw(graph.scale, graph.config, graph.svg, this.dataTarget);
        return this;
    }
}

export default Line;