import * as d3 from "d3";
import { Shape } from "../../../core";
import { getDefaultSVGProps } from "../../../core/Shape";
import constants, { SHAPES } from "../../../helpers/constants";
import errors from "../../../helpers/errors";
import {
legendClickHandler,
legendHoverHandler,
loadLegendItem
} from "../../../helpers/legend";
import styles from "../../../helpers/styles";
import utils from "../../../helpers/utils";
import {
getColorForTarget,
getShapeForTarget
} from "../../Graph/helpers/helpers";
import {
dataPointActionHandler,
drawDataPoints,
renderSelectionPath
} from "./datapointHelpers";
import { transformPoint } from "./translateHelpers";
/**
* @typedef Gantt
*/
/**
* 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.
*
* @private
* @param {object} graphConfig - config object of Graph API
* @param {object} trackLabel -Track label
* @param {object} dataTarget - Data points object
* @returns {object} dataTarget - Updated data target object
*/
export const processActionItems = (graphConfig, trackLabel, dataTarget) => {
const checkX = (x) => {
if (!utils.isDate(x)) {
throw new Error(errors.THROW_MSG_INVALID_FORMAT_TYPE);
}
return utils.parseDateTime(x);
};
const getActionLegendDetails = (legendItems, key) => {
const item = legendItems.filter((l) => l.key === key);
if (!item.length) {
throw new Error(errors.THROW_MSG_UNIQUE_ACTION_KEY_NOT_PROVIDED);
}
return item[0];
};
const legendItem = getActionLegendDetails(
graphConfig.actionLegend,
dataTarget.key
);
return dataTarget.values.map((value) => ({
key: dataTarget.key,
onClick: dataTarget.onClick,
x: checkX(value),
y: trackLabel.display,
label: legendItem.label,
color: legendItem.color || constants.DEFAULT_COLOR,
shape: legendItem.shape || SHAPES.CIRCLE,
clickPassThrough: utils.isDefined(graphConfig.clickPassThrough)
? graphConfig.clickPassThrough.actions
: false
}));
};
/**
* Updates the array parameter, with the key. If the key is present then
* it removed, else added to the array.
*
* @private
* @param {Array} shownTargets - List of targets shown in the graph
* @param {object} key - unique data set key
* @returns {Array} modified shownTarget array
*/
const updateShownTarget = (shownTargets, key) => {
const index = shownTargets.indexOf(key);
if (index > -1) {
shownTargets.splice(index, 1);
} else {
shownTargets.push(key);
}
return shownTargets;
};
/**
* Click handler for legend item. Removes the action items from graph when clicked and calls redraw
*
* @private
* @param {object} graphContext - Graph instance
* @param {object} config - Graph config object derived from input JSON
* @param {d3.selection} canvasSVG - d3 selection node of canvas svg
* @returns {Function} - returns callback function that handles click action on legend item
*/
export const clickHandler = (graphContext, config, canvasSVG) => (
element,
item
) => {
legendClickHandler(element);
updateShownTarget(config.shownTargets, item);
canvasSVG
.selectAll(`.${styles.point}[aria-describedby="${item.key}"]`)
.attr("aria-hidden", function () {
return !(d3.select(this).attr("aria-hidden") === "true");
});
};
/**
* Hover handler for legend item. Blurs the rest of the targets in Graph
* if present.
*
* @private
* @param {Array} graphTargets - List of all the items in the Graph
* @param {d3.selection} canvasSVG - d3 selection node of canvas svg
* @returns {Function} - returns callback function that handles hover action on legend item
*/
export const hoverHandler = (graphTargets, canvasSVG) => (item, state) => {
const additionalHoverHandler = (
shownTargets,
canvasSVG,
currentKey,
hoverState,
k
) =>
canvasSVG
.selectAll(`.${styles.point}[aria-describedby="${k}"]`)
.classed(styles.blur, state === constants.HOVER_EVENT.MOUSE_ENTER);
legendHoverHandler(graphTargets, canvasSVG, item.key, state, [
additionalHoverHandler
]);
};
/**
* Prepares click and hover handlers for action legend items.
*
* @private
* @param {Gantt} control - gantt chart instance
* @param {object} config - Graph config object derived from input JSON
* @param {d3.selection} canvasSVG - d3 html element of the canvas
* @param {Array} shownTargets - graph targets config object
* @returns {{clickHandler: Function, hoverHandler: Function}} - event handlers for legend items
*/
export const prepareLegendEventHandlers = (
control,
config,
canvasSVG,
shownTargets
) => ({
clickHandler: clickHandler(control, config, canvasSVG),
hoverHandler: hoverHandler(shownTargets, canvasSVG)
});
/**
* A callback that will be sent to Graph class so that when graph is
* created the Graph API will execute this callback function and the legend
* items are loaded.
*
* @private
* @param {object} config - Graph config object derived from input JSON
* @param {object} eventHandlers - Object containing click and hover event handlers for legend item
* @param {object} dataTarget - Data points object
* @param {object} legendSVG - d3 element that will be need to render the legend
* items into.
* @returns {undefined} - returns nothing
*/
export const renderLegendItems = (
config,
eventHandlers,
dataTarget,
legendSVG
) => {
if (dataTarget.label && dataTarget.label.display && legendSVG) {
loadLegendItem(legendSVG, dataTarget, config, eventHandlers);
}
};
/**
* 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_NO_DATA_POINTS}
* @param {object} content - Newly added graph content
* @returns {undefined} - returns nothing
*/
const validateActionContent = (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.values)) {
throw new Error(errors.THROW_MSG_NO_DATA_POINTS);
}
};
/**
* 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 loadActionInput = (inputJSON) => {
validateActionContent(inputJSON);
return utils.deepClone(inputJSON);
};
/**
* Renders the data point in the provided path element.
* It uses the consumer opted Shape, color of the data point.
* Behavior when clicked on the data point etc.
*
* @private
* @param {object} scale - d3 scale for Graph
* @param {object} config - Graph config object derived from input JSON
* @param {SVGElement} path - svg Path element
* @param {object} dataPoint - data point properties such as shape, color and onClick callback function
* @param {number} index - data point index
* @returns {object} - d3 selection object
*/
const renderDataPointPath = (scale, config, path, dataPoint, index) =>
path.append(() =>
new Shape(getShapeForTarget(dataPoint)).getShapeElement(
getDefaultSVGProps({
svgClassNames: styles.point,
svgStyles: `fill: ${getColorForTarget(dataPoint)};`,
transformFn: transformPoint(scale, config)(dataPoint),
onClickFn() {
dataPointActionHandler(dataPoint, index, this);
},
a11yAttributes: {
"aria-hidden": document.querySelector(
`li[aria-describedby="${dataPoint.key}"]`
)
? document
.querySelector(
`li[aria-describedby="${dataPoint.key}"]`
)
.getAttribute("aria-current") === "false"
: false,
"aria-describedby": dataPoint.key,
"aria-disabled": !utils.isFunction(dataPoint.onClick)
},
additionalAttributes: {
"pointer-events":
dataPoint.clickPassThrough &&
!utils.isFunction(dataPoint.onClick)
}
})
)
);
/**
* Draws the points with options opted in the input JSON by the consumer for each data set.
* Render the point with appropriate color, shape, x and y co-ordinates, label etc.
* On click content callback function is called.
*
* @private
* @param {object} scale - d3 scale for Graph
* @param {object} config - Graph config object derived from input JSON
* @param {d3.selection} canvasSVG - d3 html element of the canvas
* @returns {object} - d3 append object
*/
const drawActionDataPoints = (scale, config, canvasSVG) =>
canvasSVG
.append("g")
.classed(styles.pointGroup, true)
.each(function (dataPoint, index) {
const dataPointSVG = d3.select(this);
renderSelectionPath(scale, config, dataPointSVG, dataPoint, index);
renderDataPointPath(scale, config, dataPointSVG, dataPoint, index);
});
/**
* Creates an element container with data points from the input JSON property: action
*
* @private
* @param {object} graphContext - Gantt instance
* @param {object} trackPathSVG - Track container element
* @param {object} trackLabel - Track label
* @param {object} gantt - input config for creating an action
* @returns {undefined} - returns nothing
*/
const loadActions = (graphContext, trackPathSVG, trackLabel, gantt) =>
gantt.actions.forEach((a, i) => {
drawDataPoints(
graphContext.scale,
graphContext.config,
trackPathSVG,
processActionItems(
graphContext.config,
trackLabel,
loadActionInput(a)
),
drawActionDataPoints,
false
);
gantt.actionKeys.splice(i, 0, a.key);
});
/**
* Update activities for the track.
*
* @private
* @param {object} config - Graph config object derived from input JSON
* @param {object} scale - d3 scale for Graph
* @param {object} gantt - Graph config object for the content.
* @param {object} trackGroupPath - Container for the track
* @returns {undefined} - returns nothing
*/
const reflowActions = (config, scale, gantt, trackGroupPath) => {
gantt.config.actions.forEach((action) => {
validateActionContent(action);
});
trackGroupPath
.selectAll(`.${styles.currentPointsGroup}[event="false"]`)
.remove();
gantt.config.actions.forEach((action) => {
drawDataPoints(
scale,
config,
trackGroupPath,
processActionItems(
config,
gantt.config.trackLabel,
loadActionInput(action)
),
drawActionDataPoints,
false
);
});
};
/**
* Selects all the data point groups from the track and removes them
*
* @private
* @param {object} graphContext - Gantt instance
* @param {object} trackPathSVG - Track container element
* @returns {Selection} - track container element
*/
const unloadActions = (graphContext, trackPathSVG) =>
trackPathSVG.selectAll(`g.${styles.currentPointsGroup}`).remove();
export { loadActions, unloadActions, reflowActions };