controls/PairedResult/PairedResult.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 {
    hideAllRegions,
    removeRegion,
    translateRegion,
    areRegionsIdentical
} from "../../helpers/region";
import styles from "../../helpers/styles";
import utils from "../../helpers/utils";
import {
    clear,
    clickHandler,
    draw,
    drawLine,
    drawPoints,
    getValue,
    getDataPointValues,
    hoverHandler,
    iterateOnPairType,
    prepareLegendItems,
    processDataPoints,
    renderRegion,
    isRegionMappedToAllValues,
    translatePairedResultGraph,
    isSinglePairedResultTargetDisplayed
} from "./helpers/helpers";
import { drawSelectionIndicator } from "./helpers/selectionIndicatorHelpers";
import PairedResultConfig from "./PairedResultConfig";
import {
    calculateVerticalPadding,
    getXAxisXPosition
} from "../../helpers/axis";

/**
 * @typedef {object} PairedResult
 * @typedef {object} GraphContent
 * @typedef {object} PairedResultConfig
 */
/**
 * Calculates the min and max values for Y Axis or Y2 Axis
 *
 * @private
 * @param {Array} values - Datapoint values
 * @param {string} axis - y or y2
 * @returns {object} - Contains min and max values for the data points
 */
const calculateValuesRange = (values, axis = constants.Y_AXIS) => ({
    [axis]: {
        min: Math.min(
            ...values.map((i) => Math.min(...Object.keys(i).map((j) => i[j].y)))
        ),
        max: Math.max(
            ...values.map((i) => Math.max(...Object.keys(i).map((j) => i[j].y)))
        )
    }
});

/**
 * 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} PairedResultConfig config object containing consumer data
 */
const loadInput = (inputJSON) =>
    new PairedResultConfig()
        .setInput(inputJSON)
        .validateInput()
        .clone()
        .getConfig();

/**
 * A Paired result graph is a graph that is represented by 2 points
 * and a line connecting them. There may be an optional 3rd datapoint
 * representing a median between them.
 *
 * @example
 * You can have 3 pairs of x and y co-ordinates with different x and y values to make option 3 below.
 * Or
 * You can have 3 identical X co-ordinates with varying Y co-ordinates to have option 1, shown below.
 *   o
 *   |
 *   |
 *   |
 *   |
 *   o
 *
 *  // Or
 *
 * o------------o
 *
 * // Or
 * o
 *  \
 *   \
 *    \
 *     \
 *      o
 *
 * // etc.
 * Lifecycle functions include:
 *  * Load
 *  * Generate
 *  * Unload
 *  * Destroy
 * @module PairedResult
 * @class PairedResult
 */
class PairedResult extends GraphContent {
    /**
     * @class
     * @param {PairedResultConfig} input - Input JSON instance created using GraphConfig
     */
    constructor(input) {
        super();
        this.config = loadInput(input);
        this.type = "PairedResult";
        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) &&
            (utils.notEmpty(this.dataTarget.regions) ||
                utils.notEmpty(this.dataTarget.valueRegionSubset))
        ) {
            renderRegion(graph.scale, graph.config, graph.svg, this.dataTarget);
        }
        prepareLegendItems(
            graph.config,
            {
                clickHandler: clickHandler(
                    graph,
                    this,
                    graph.config,
                    graph.svg
                ),
                hoverHandler: hoverHandler(graph.config, graph.svg)
            },
            this.dataTarget,
            graph.legendSVG
        );
        if (graph.axesLabelShapeGroup[this.config.yAxis]) {
            iterateOnPairType((type) => {
                prepareLabelShapeItem(
                    graph.config,
                    {
                        key: `${this.dataTarget.key}_${type}`,
                        label: getValue(this.dataTarget.label, type),
                        color: getValue(this.dataTarget.color, type),
                        shape: getValue(this.dataTarget.shape, type),
                        values: this.dataTarget.values
                    },
                    graph.axesLabelShapeGroup[this.config.yAxis]
                );
            });
        }
        return this;
    }

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

    /**
     * @inheritdoc
     */
    resize(graph) {
        if (utils.notEmpty(this.dataTarget.values)) {
            if (
                utils.notEmpty(this.dataTarget.regions) ||
                utils.notEmpty(this.dataTarget.valueRegionSubset)
            ) {
                const values = this.dataTarget.values;
                if (isSinglePairedResultTargetDisplayed(graph.config, graph)) {
                    graph.config.shouldHideAllRegion = false;
                } else if (graph.content.length > 1) {
                    // If graph has more than 1 content, we compare the regions if they are identical show and hide if even atleast one of them is not.
                    // check if paired Data is proper i.e - region for each key(high, mid and low) in value should be there
                    const isPairedDataProper = values.every((value) =>
                        isRegionMappedToAllValues(
                            value,
                            this.dataTarget.regions ||
                                this.dataTarget.valueRegionSubset
                        )
                    );

                    graph.config.shouldHideAllRegion =
                        !isPairedDataProper ||
                        graph.config.shouldHideAllRegion ||
                        !areRegionsIdentical(graph.svg);
                }

                translateRegion(
                    graph.scale,
                    graph.config,
                    graph.svg.select(
                        `g[aria-describedby="region_${this.dataTarget.key}"]`
                    ),
                    this.dataTarget.yAxis,
                    utils.notEmpty(this.dataTarget.valueRegionSubset)
                );
            } else {
                graph.config.shouldHideAllRegion = true;
            }
            if (graph.config.shouldHideAllRegion) {
                hideAllRegions(graph.svg);
            }
        }

        translatePairedResultGraph(graph.scale, graph.svg, graph.config);
        return this;
    }

    /**
     * @inheritdoc
     */
    reflow(graph, graphData) {
        const eventHandlers = {
            clickHandler: clickHandler(graph, this, graph.config, graph.svg),
            hoverHandler: hoverHandler(graph.config, graph.svg)
        };
        const constructLegendLabels = (d, type) =>
            Object.assign(
                {},
                {
                    shape: getValue(d.shape, type),
                    color: getValue(d.color, type),
                    label: getValue(d.label, type),
                    key: `${d.key}_${type}`,
                    values: d.values,
                    legendOptions: d.legendOptions,
                    type
                }
            );
        const reflow = !!this.config.values.length;
        this.config.values = graphData.values;
        this.dataTarget = processDataPoints(graph.config, this.config, reflow);
        const drawBox = (boxPath) => {
            drawSelectionIndicator(graph.scale, graph.config, boxPath);
            drawLine(graph.scale, graph.config, boxPath);
            drawPoints(graph.scale, graph.config, boxPath);
        };
        const types = ["high", "mid", "low"];
        types.forEach((type) => {
            const label = getValue(graph.contentConfig[0].label, type);
            if (label && label.display) {
                reflowLegend(
                    graph.legendSVG,
                    constructLegendLabels(
                        graph.contentConfig.filter(
                            (pairedResult) => pairedResult.key === graphData.key
                        )[0],
                        type
                    ),
                    graph,
                    eventHandlers
                );
            }
        });
        if (utils.isEmpty(graphData.values)) {
            iterateOnPairType((type) => {
                const key = `${this.dataTarget.key}_${type}`;
                removeLabelShapeItem(
                    graph.axesLabelShapeGroup[this.config.yAxis],
                    {
                        key
                    },
                    graph.config
                );
            });
        }
        const internalValuesSubset = getDataPointValues(this.dataTarget);
        graph.svg
            .select(`g[aria-describedby="${graphData.key}"]`)
            .selectAll(`.${styles.pairedBox}`)
            .remove();
        const pairedBoxSVG = graph.svg
            .select(`g[aria-describedby="${graphData.key}"]`)
            .selectAll(`.${styles.pairedBox}`)
            .data(internalValuesSubset);
        pairedBoxSVG
            .enter()
            .append("g")
            .classed(styles.pairedBox, true)
            .attr("aria-selected", false)
            .attr(
                "transform",
                `translate(${getXAxisXPosition(
                    graph.config
                )},${calculateVerticalPadding(graph.config)})`
            )
            .call(drawBox);
        pairedBoxSVG.exit().remove();

        this.valuesRange = calculateValuesRange(
            this.config.values,
            this.config.yAxis
        );
        this.resize(graph);
    }

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

export default PairedResult;