controls/Timeline/TimelineContent.js

"use strict";
/**
 * @typedef {object} TimelineContent
 * @typedef {object} GraphContent
 * @typedef {object} TimelineConfig
 */
import { GraphContent } from "../../core";
import constants, { SHAPES } from "../../helpers/constants";
import errors from "../../helpers/errors";
import { removeLegendItem } from "../../helpers/legend";
import styles from "../../helpers/styles";
import utils from "../../helpers/utils";
import { isUniqueKey } from "../Graph/GraphConfig";
import {
    clear,
    clickHandler,
    createPoints,
    createTimelineContentGroup,
    hoverHandler,
    prepareLegendItems
} from "./helpers/creationHelpers";
import { translatePoints } from "./helpers/translateHelpers";

/**
 * Checks if values are of type datetime in ISO8601 datetime format
 *
 * @private
 * @param {string} x - Date string
 * @returns {Date} ISO8601 datetime
 */
const getXDataValues = (x) => {
    if (!utils.isDate(x)) {
        throw new Error(errors.THROW_MSG_INVALID_FORMAT_TYPE);
    }
    return utils.parseDateTime(x);
};
/**
 * Processes the input JSON and adds the shapes, colors, labels etc. to each data points so that we
 * can use them when rendering the data point. Applies isCritical to the point if point is
 * marked critical.
 *
 * @private
 * @param {object} graphConfig - config object of Graph API
 * @param {object} dataTarget - Data points object
 * @returns {object} dataTarget - Updated data target object
 */
const processDataPoints = (graphConfig, dataTarget) => {
    graphConfig.shownTargets.push(dataTarget.key);
    dataTarget.internalValuesSubset = dataTarget.values.map((value) => ({
        onClick: dataTarget.onClick,
        isCritical: value.isCritical || false,
        x: getXDataValues(value.x),
        content: value.content,
        color: dataTarget.color || constants.DEFAULT_COLOR,
        label: dataTarget.label || {},
        shape: dataTarget.shape || SHAPES.CIRCLE,
        key: dataTarget.key
    }));
    return dataTarget;
};
/**
 * Validates the newly added timeline content into the graph before rendering
 *
 * @private
 * @param {object} input - Newly added graph tasks
 * @throws {module:errors.THROW_MSG_NO_DATA_LOADED}
 * @throws {module:errors.THROW_MSG_NO_DATA_POINTS}
 * @throws {module:errors.THROW_MSG_INVALID_DATA_PROPERTY}
 * @returns {undefined} - returns nothing
 */
export const validateContent = (input) => {
    if (utils.isEmpty(input)) {
        throw new Error(errors.THROW_MSG_NO_DATA_LOADED);
    }
    if (utils.isEmpty(input.key)) {
        throw new Error(errors.THROW_MSG_UNIQUE_KEY_NOT_PROVIDED);
    }
    if (utils.isEmpty(input.label) || utils.isEmpty(input.label.display)) {
        throw new Error(errors.THROW_MSG_UNIQUE_LABEL_NOT_PROVIDED);
    }
    if (utils.isEmpty(input.values)) {
        throw new Error(errors.THROW_MSG_NO_DATA_POINTS);
    }
};
/**
 * Draws points along X Axis based on datetime
 *
 * @private
 * @param {TimelineContent} control - TimelineContent instance
 * @param {object} scale - d3 scale taking into account the input parameters
 * @param {object} config - config object derived from input JSON
 * @param {Array} timelineGraphContentSVG - d3 selection node of canvas svg
 * @param {Array} dataTarget - Data points
 * @returns {undefined} - returns nothing
 */
const draw = (control, scale, config, timelineGraphContentSVG, dataTarget) => {
    const contentGroupPath = createTimelineContentGroup(
        config,
        timelineGraphContentSVG,
        control.config
    );
    const pointPath = contentGroupPath
        .selectAll(`.${styles.point}`)
        .data(dataTarget.internalValuesSubset);
    createPoints(scale, config, pointPath.enter());
    pointPath.exit().transition().call(constants.d3Transition).remove();
};
/**
 * 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} config object containing consumer data
 */
const loadInput = (inputJSON) => {
    validateContent(inputJSON);
    return utils.deepClone(inputJSON);
};

/**
 * TimelineContent is part of a Timeline graph
 *
 * @module TimelineContent
 * @class TimelineContent
 */
class TimelineContent extends GraphContent {
    /**
     * @class
     * @param {object} input - Input JSON
     */
    constructor(input) {
        super();
        this.config = loadInput(input);
        this.dataTarget = null;
    }

    /**
     * @inheritdoc
     */
    load(graph) {
        if (!isUniqueKey(graph.contentConfig, this.config.key)) {
            throw new Error(errors.THROW_MSG_UNIQUE_KEY_NOT_PROVIDED);
        }
        this.dataTarget = processDataPoints(graph.config, this.config);
        draw(
            this,
            graph.scale,
            graph.config,
            graph.svg.select(`.${styles.timelineGraphContent}`),
            this.dataTarget
        );
        prepareLegendItems(
            graph.config,
            {
                clickHandler: clickHandler(
                    graph,
                    this,
                    graph.config,
                    graph.svg
                ),
                hoverHandler: hoverHandler(graph.config.shownTargets, graph.svg)
            },
            this.dataTarget,
            graph.legendSVG
        );
        return this;
    }

    /**
     * @inheritdoc
     */
    unload(graph) {
        clear(graph.svg, this.dataTarget);
        removeLegendItem(graph.legendSVG, this.dataTarget);
        this.dataTarget = {};
        this.config = {};
        return this;
    }

    /**
     * @inheritdoc
     */
    resize(graph) {
        translatePoints(
            graph.scale,
            graph.config,
            graph.svg,
            this.config.key,
            `.${styles.point}`
        );
        translatePoints(
            graph.scale,
            graph.config,
            graph.svg,
            this.config.key,
            `.${styles.dataPointSelection}`
        );
        return this;
    }

    /**
     * @inheritdoc
     */
    reflow(graph, graphData) {
        this.config.values = graphData.values;
        this.dataTarget = processDataPoints(graph.config, this.config);
        const position = graph.config.shownTargets.lastIndexOf(graphData.key);
        if (position > -1) {
            graph.config.shownTargets.splice(position, 1);
        }
        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}"]`)
            .selectAll(`[class*="${styles.point}"]`)
            .data(this.dataTarget.internalValuesSubset);
        createPoints(graph.scale, graph.config, pointPath.enter());
        pointPath
            .exit()
            .transition()
            .call(
                constants.d3Transition(
                    graph.config.settingsDictionary.transition
                )
            )
            .remove();
        this.resize(graph);
    }

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

export default TimelineContent;