controls/Gantt/Track.js

"use strict";
/**
 * @typedef {object} Track
 * @typedef {object} GraphContent
 * @typedef {object} GanttConfig
 */
import * as d3 from "d3";
import { GraphContent } from "../../core";
import errors from "../../helpers/errors";
import { loadLabelPopup, shouldTruncateLabel } from "../../helpers/label";
import styles from "../../helpers/styles";
import utils from "../../helpers/utils";
import { isUniqueKey } from "./GanttConfig";
import {
    loadActions,
    unloadActions,
    reflowActions
} from "./helpers/actionHelpers";
import {
    loadActivities,
    unloadActivities,
    reflowActivities
} from "./helpers/activityHelpers";
import {
    createTrackContainer,
    removeTrackContainer,
    scaleGraph,
    updateTrackProps
} from "./helpers/creationHelpers";
import { loadEvents, unloadEvents, reflowEvents } from "./helpers/eventHelpers";
import { loadTasks, unloadTasks, reflowTasks } from "./helpers/taskHelpers";
import {
    loadGanttTrackSelector,
    unloadGanttTrackSelector
} from "./helpers/trackHelpers";
import {
    translateActivities,
    translateDataPoints,
    translateTasks,
    translateTrackSelector
} from "./helpers/translateHelpers";

/**
 * Validates the newly added content into the graph before rendering
 *
 * @private
 * @throws {module:errors.THROW_MSG_NO_DATA_LOADED}
 * @throws {module:errors.THROW_MSG_UNIQUE_KEY_NOT_PROVIDED}
 * @throws {module:errors.THROW_MSG_UNIQUE_LABEL_NOT_PROVIDED}
 * @throws {module:errors.THROW_MSG_NON_UNIQUE_PROPERTY}
 * @throws {module:errors.THROW_MSG_TASKS_NOT_PROVIDED}
 * @param {object} content - Newly added graph content
 * @returns {undefined} - returns nothing
 */
export const validateContent = (content) => {
    if (utils.isEmpty(content)) {
        throw new Error(errors.THROW_MSG_NO_DATA_LOADED);
    }
    if (utils.isEmpty(content.key)) {
        throw new Error(errors.THROW_MSG_UNIQUE_KEY_NOT_PROVIDED);
    }
    if (
        utils.isEmpty(content.trackLabel) ||
        utils.isEmpty(content.trackLabel.display)
    ) {
        throw new Error(errors.THROW_MSG_UNIQUE_LABEL_NOT_PROVIDED);
    }
};
/**
 * Checks if the track name is same as the instance track name.
 * We are doing this since tick texts cannot be appended with a unique attribute like
 * the rest of the implementation. D3 directly renders the tick elements along with the
 * text based on the domain array values.
 *
 * @private
 * @param {Track} control - Track instance
 * @param {string} display - current track instance Label value
 * @returns {boolean} true if label value is the same as the tick text, false otherwise
 */
const hasMatchingTrackLabel = (control, display) =>
    d3.select(control).datum() === display;
/**
 * Adds onClick event for each tick.
 * Criteria:
 *  * Text needs to have a unique display value
 *  * Text needs to have a valid function for onClick
 *
 * @private
 * @param {d3.selection} canvasSVG - d3 selection node of canvas svg
 * @param {object} labelObj - Label object provided by the consumer
 * @returns {object} d3 svg path
 */
const addTrackLabelEventHandler = (canvasSVG, labelObj) =>
    canvasSVG
        .selectAll(`.${styles.axisYTrackLabel} .tick text`)
        .each(function (displayVal) {
            // checks if track name is same as instance of track name
            if (!displayVal || !hasMatchingTrackLabel(this, labelObj.display)) {
            } else {
                shouldTruncateLabel(labelObj.display)
                    ? d3
                          .select(this)
                          .style("cursor", "pointer")
                          .on("click", () => {
                              // If the consumer provides onclick function, it will override default onClick functionality provided by Carbon-graphs
                              utils.isDefined(labelObj.onClick) &&
                              utils.isFunction(labelObj.onClick)
                                  ? labelObj.onClick(
                                        labelObj.display,
                                        d3.select(this)
                                    )
                                  : loadLabelPopup(labelObj.display, "y");
                          })
                    : null;
            }
        });

/**
 * 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) => utils.deepClone(inputJSON);

/**
 * Track sub-graph part of a Gantt chart, which forms the foreground task
 *
 * @module Track
 * @class Track
 */
class Track extends GraphContent {
    /**
     * @class
     * @param {object} input - Input JSON
     */
    constructor(input) {
        super();
        this.config = loadInput(input);
        this.config.actionKeys = [];
        this.config.eventKeys = [];
        this.config.activityKeys = [];
        this.config.taskKeys = [];
        this.trackGroupPath = null;
    }

    /**
     * @inheritdoc
     */
    load(graph) {
        if (!isUniqueKey(graph.tracks, this.config.key)) {
            throw new Error(errors.THROW_MSG_UNIQUE_KEY_NOT_PROVIDED);
        }
        this.trackGroupPath = createTrackContainer(
            graph.config,
            graph.svg,
            this.config
        );
        /**
         * To load the gantt track selector, we need to find the track-height, this gets updated only when updateTrackProps gets called.
         *
         * We Need to loadGanttTrackSelector prior to all other components, since trackSelector is part of trackGroup and should be layered bottom,
         * to ensure the clickable functionality.
         */
        updateTrackProps(graph.config, this.config, true);
        loadGanttTrackSelector(graph, this.trackGroupPath, this.config);
        if (utils.notEmpty(this.config.activities)) {
            loadActivities(
                graph,
                this.trackGroupPath,
                this.config.trackLabel,
                this.config
            );
        }
        if (utils.notEmpty(this.config.tasks)) {
            loadTasks(
                graph,
                this.trackGroupPath,
                this.config.trackLabel,
                this.config
            );
        }
        if (utils.notEmpty(this.config.events)) {
            loadEvents(
                graph,
                this.trackGroupPath,
                this.config.trackLabel,
                this.config
            );
        }
        if (utils.notEmpty(this.config.actions)) {
            loadActions(
                graph,
                this.trackGroupPath,
                this.config.trackLabel,
                this.config
            );
        }
        return this;
    }

    /**
     * @inheritdoc
     */
    unload(graph) {
        updateTrackProps(graph.config, this.config);
        unloadGanttTrackSelector(graph, this.trackGroupPath);
        if (utils.notEmpty(this.config.activities)) {
            unloadActivities(graph, this.trackGroupPath);
        }
        if (utils.notEmpty(this.config.tasks)) {
            unloadTasks(graph, this.trackGroupPath);
        }
        if (utils.notEmpty(this.config.events)) {
            unloadEvents(graph, this.trackGroupPath);
        }
        if (utils.notEmpty(this.config.actions)) {
            unloadActions(graph, this.trackGroupPath);
        }
        removeTrackContainer(graph.svg, this.config.key);
        this.config = {};
        return this;
    }

    /**
     * @inheritdoc
     */
    resize(graph) {
        scaleGraph(graph.scale, graph.config);
        if (utils.notEmpty(this.trackGroupPath)) {
            translateTrackSelector(
                graph.scale,
                graph.config,
                this.trackGroupPath,
                this.config
            );
        }
        if (utils.notEmpty(this.config.activities)) {
            translateActivities(graph.scale, graph.config, this.trackGroupPath);
        }
        if (utils.notEmpty(this.config.tasks)) {
            translateTasks(graph.scale, graph.config, this.trackGroupPath);
        }
        if (
            utils.notEmpty(this.config.actions) ||
            utils.notEmpty(this.config.events)
        ) {
            translateDataPoints(graph.scale, graph.config, this.trackGroupPath);
        }
        return this;
    }

    /**
     * @inheritdoc
     */
    reflow(graph, graphData) {
        if (
            utils.notEmpty(graphData.activities) &&
            utils.notEmpty(this.config.activities)
        ) {
            graphData.activityKeys = [];
            graphData.activities.forEach((activity) => {
                graphData.activityKeys.push(activity.key);
                if (this.config.activityKeys.includes(activity.key)) {
                    const position = this.config.activityKeys.indexOf(
                        activity.key
                    );
                    this.config.activities[position].startDate =
                        activity.startDate;
                    this.config.activities[position].endDate = activity.endDate;
                }
            });
            this.config.activityKeys.slice(0).forEach((key, i) => {
                if (!graphData.activityKeys.includes(key)) {
                    const position = this.config.activityKeys.indexOf(key);
                    this.config.activityKeys.splice(position, 1);
                    this.config.activities.splice(position, 1);
                }
            });
            const trackGroupPath = graph.svg.selectAll(
                `.${styles.trackGroup}[aria-describedby="${this.config.key}"]`
            );
            reflowActivities(
                graph.svg,
                graph.config,
                graph.scale,
                this,
                trackGroupPath
            );
        }
        if (
            utils.notEmpty(graphData.tasks) &&
            utils.notEmpty(this.config.tasks)
        ) {
            graphData.taskKeys = [];
            graphData.tasks.forEach((task) => {
                graphData.taskKeys.push(task.key);
                if (this.config.taskKeys.includes(task.key)) {
                    const position = this.config.taskKeys.indexOf(task.key);
                    this.config.tasks[position].startDate = task.startDate;
                    this.config.tasks[position].endDate = task.endDate;
                }
            });
            this.config.taskKeys.slice(0).forEach((key, i) => {
                if (!graphData.taskKeys.includes(key)) {
                    const position = this.config.taskKeys.indexOf(key);
                    this.config.taskKeys.splice(position, 1);
                    this.config.tasks.splice(position, 1);
                }
            });
            const trackGroupPath = graph.svg.selectAll(
                `.${styles.trackGroup}[aria-describedby="${this.config.key}"]`
            );
            reflowTasks(
                graph.svg,
                graph.config,
                graph.scale,
                this,
                trackGroupPath
            );
        }
        if (
            utils.notEmpty(graphData.events) &&
            utils.notEmpty(this.config.events)
        ) {
            graphData.eventKeys = [];
            graphData.events.forEach((event) => {
                graphData.eventKeys.push(event.key);
                if (this.config.eventKeys.includes(event.key)) {
                    const position = this.config.eventKeys.indexOf(event.key);
                    this.config.events[position].values = event.values;
                }
            });
            this.config.eventKeys.slice(0).forEach((key) => {
                if (!graphData.eventKeys.includes(key)) {
                    const position = this.config.eventKeys.indexOf(key);
                    this.config.eventKeys.splice(position, 1);
                    this.config.events.splice(position, 1);
                }
            });
            const trackGroupPath = graph.svg.selectAll(
                `.${styles.trackGroup}[aria-describedby="${this.config.key}"]`
            );
            reflowEvents(graph.config, graph.scale, this, trackGroupPath);
        }
        if (
            utils.notEmpty(graphData.actions) &&
            utils.notEmpty(this.config.actions)
        ) {
            graphData.actionKeys = [];
            graphData.actions.forEach((action) => {
                graphData.actionKeys.push(action.key);
                if (this.config.actionKeys.includes(action.key)) {
                    const position = this.config.actionKeys.indexOf(action.key);
                    this.config.actions[position].values = action.values;
                }
            });
            this.config.actionKeys.slice(0).forEach((key) => {
                if (!graphData.actionKeys.includes(key)) {
                    const position = this.config.actionKeys.indexOf(key);
                    this.config.actionKeys.splice(position, 1);
                    this.config.actions.splice(position, 1);
                }
            });
            const trackGroupPath = graph.svg.selectAll(
                `.${styles.trackGroup}[aria-describedby="${this.config.key}"]`
            );
            reflowActions(graph.config, graph.scale, this, trackGroupPath);
        }

        this.resize(graph);
    }

    /**
     * @inheritdoc
     */
    redraw(graph) {
        addTrackLabelEventHandler(graph.svg, this.config.trackLabel);
        return this;
    }
}

export default Track;