almus-d3-graph
Version:
React component to build interactive and configurable graphs with d3 effortlessly
460 lines (386 loc) • 19.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.updateNodeHighlightedValue = exports.initializeGraphState = exports.getCenterAndZoomTransformation = exports.checkForGraphElementsChanges = exports.checkForGraphConfigChanges = undefined;
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; /**
* @module Graph/helper
* @description
* Offers a series of methods that isolate logic of Graph component and also from Graph rendering methods.
*/
/**
* @typedef {Object} Link
* @property {string} source - the node id of the source in the link.
* @property {string} target - the node id of the target in the link.
* @memberof Graph/helper
*/
/**
* @typedef {Object} Node
* @property {string} id - the id of the node.
* @property {string} [color=] - color of the node (optional).
* @property {string} [fontColor=] - node text label font color (optional).
* @property {string} [size=] - size of the node (optional).
* @property {string} [symbolType=] - symbol type of the node (optional).
* @property {string} [svg=] - custom svg for node (optional).
* @memberof Graph/helper
*/
var _d3Force = require("d3-force");
var _graph2 = require("./graph.const");
var _graph3 = _interopRequireDefault(_graph2);
var _graph4 = require("./graph.config");
var _graph5 = _interopRequireDefault(_graph4);
var _err = require("../../err");
var _err2 = _interopRequireDefault(_err);
var _utils = require("../../utils");
var _utils2 = _interopRequireDefault(_utils);
var _collapse = require("./collapse.helper");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
var NODE_PROPS_WHITELIST = ["id", "highlighted", "x", "y", "index", "vy", "vx"];
var LINK_CUSTOM_PROPS_WHITELIST = ["color", "opacity", "strokeWidth", "label"];
/**
* Create d3 forceSimulation to be applied on the graph.<br/>
* {@link https://github.com/d3/d3-force#forceSimulation|d3-force#forceSimulation}<br/>
* {@link https://github.com/d3/d3-force#simulation_force|d3-force#simulation_force}<br/>
* Wtf is a force? {@link https://github.com/d3/d3-force#forces| here}
* @param {number} width - the width of the container area of the graph.
* @param {number} height - the height of the container area of the graph.
* @param {number} gravity - the force strength applied to the graph.
* @returns {Object} returns the simulation instance to be consumed.
* @memberof Graph/helper
*/
function _createForceSimulation(width, height, gravity) {
var frx = (0, _d3Force.forceX)(width / 2).strength(_graph3.default.FORCE_X);
var fry = (0, _d3Force.forceY)(height / 2).strength(_graph3.default.FORCE_Y);
var forceStrength = gravity;
return (0, _d3Force.forceSimulation)().force("charge", (0, _d3Force.forceManyBody)().strength(forceStrength)).force("x", frx).force("y", fry);
}
/**
* Receives a matrix of the graph with the links source and target as concrete node instances and it transforms it
* in a lightweight matrix containing only links with source and target being strings representative of some node id
* and the respective link value (if non existent will default to 1).
* @param {Array.<Link>} graphLinks - an array of all graph links.
* @param {Object} config - the graph config.
* @returns {Object.<string, Object>} an object containing a matrix of connections of the graph, for each nodeId,
* there is an object that maps adjacent nodes ids (string) and their values (number).
* @memberof Graph/helper
*/
function _initializeLinks(graphLinks, config) {
return graphLinks.reduce(function (links, l) {
var source = l.source.id !== undefined && l.source.id !== null ? l.source.id : l.source;
var target = l.target.id !== undefined && l.target.id !== null ? l.target.id : l.target;
if (!links[source]) {
links[source] = {};
}
if (!links[target]) {
links[target] = {};
}
var value = config.collapsible && l.isHidden ? 0 : l.value || 1;
links[source][target] = value;
if (!config.directed) {
links[target][source] = value;
}
return links;
}, {});
}
/**
* Method that initialize graph nodes provided by rd3g consumer and adds additional default mandatory properties
* that are optional for the user. Also it generates an index mapping, this maps nodes ids the their index in the array
* of nodes. This is needed because d3 callbacks such as node click and link click return the index of the node.
* @param {Array.<Node>} graphNodes - the array of nodes provided by the rd3g consumer.
* @returns {Object.<string, Object>} returns the nodes ready to be used within rd3g with additional properties such as x, y
* and highlighted values.
* @memberof Graph/helper
*/
function _initializeNodes(graphNodes) {
var nodes = {};
var n = graphNodes.length;
for (var i = 0; i < n; i++) {
var node = graphNodes[i];
node.highlighted = false;
if (!node.hasOwnProperty("x")) {
node.x = 0;
}
if (!node.hasOwnProperty("y")) {
node.y = 0;
}
nodes[node.id.toString()] = node;
}
return nodes;
}
/**
* Maps an input link (with format `{ source: 'sourceId', target: 'targetId' }`) to a d3Link
* (with format `{ source: { id: 'sourceId' }, target: { id: 'targetId' } }`). If d3Link with
* given index exists already that same d3Link is returned.
* @param {Object} link - input link.
* @param {number} index - index of the input link.
* @param {Array.<Object>} d3Links - all d3Links.
* @param {Object} config - same as {@link #graphrenderer|config in renderGraph}.
* @param {Object} state - Graph component current state (same format as returned object on this function).
* @returns {Object} a d3Link.
* @memberof Graph/helper
*/
function _mapDataLinkToD3Link(link, index) {
var d3Links = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
var config = arguments[3];
var state = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {};
// find the matching link if it exists
var d3Link = d3Links.find(function (l) {
return l.source.id === link.source && l.target.id === link.target;
});
var customProps = _utils2.default.pick(link, LINK_CUSTOM_PROPS_WHITELIST);
if (d3Link) {
var toggledDirected = state.config && state.config.directed && config.directed !== state.config.directed;
var refinedD3Link = _extends({
index: index
}, d3Link, customProps);
// every time we toggle directed config all links should be visible again
if (toggledDirected) {
return _extends({}, refinedD3Link, { isHidden: false });
}
// every time we disable collapsible (collapsible is false) all links should be visible again
return config.collapsible ? refinedD3Link : _extends({}, refinedD3Link, { isHidden: false });
}
var highlighted = false;
var source = {
id: link.source,
highlighted: highlighted
};
var target = {
id: link.target,
highlighted: highlighted
};
return _extends({
index: index,
source: source,
target: target
}, customProps);
}
/**
* Tags orphan nodes with a `_orphan` flag.
* @param {Object.<string, Object>} nodes - nodes mapped by their id.
* @param {Object.<string, Object>} linksMatrix - an object containing a matrix of connections of the graph, for each nodeId,
* there is an object that maps adjacent nodes ids (string) and their values (number).
* @returns {Object.<string, Object>} same input nodes structure with tagged orphans nodes where applicable.
* @memberof Graph/helper
*/
function _tagOrphanNodes(nodes, linksMatrix) {
return Object.keys(nodes).reduce(function (acc, nodeId) {
var _computeNodeDegree = (0, _collapse.computeNodeDegree)(nodeId, linksMatrix),
inDegree = _computeNodeDegree.inDegree,
outDegree = _computeNodeDegree.outDegree;
var node = nodes[nodeId];
var taggedNode = inDegree === 0 && outDegree === 0 ? _extends({}, node, { _orphan: true }) : node;
acc[nodeId] = taggedNode;
return acc;
}, {});
}
/**
* Some integrity validations on links and nodes structure. If some validation fails the function will
* throw an error.
* @param {Object} data - Same as {@link #initializeGraphState|data in initializeGraphState}.
* @throws can throw the following error msg:
* INSUFFICIENT_DATA - msg if no nodes are provided
* INVALID_LINKS - if links point to nonexistent nodes
* @returns {undefined}
* @memberof Graph/helper
*/
function _validateGraphData(data) {
if (!data.nodes || !data.nodes.length) {
_utils2.default.throwErr("Graph", _err2.default.INSUFFICIENT_DATA);
}
var n = data.links.length;
var _loop = function _loop(i) {
var l = data.links[i];
if (!data.nodes.find(function (n) {
return n.id === l.source;
})) {
_utils2.default.throwErr("Graph", _err2.default.INVALID_LINKS + " - \"" + l.source + "\" is not a valid source node id");
}
if (!data.nodes.find(function (n) {
return n.id === l.target;
})) {
_utils2.default.throwErr("Graph", _err2.default.INVALID_LINKS + " - \"" + l.target + "\" is not a valid target node id");
}
if (l && l.value !== undefined && typeof l.value !== "number") {
_utils2.default.throwErr("Graph", _err2.default.INVALID_LINK_VALUE + " - found in link with source \"" + l.source + "\" and target \"" + l.target + "\"");
}
};
for (var i = 0; i < n; i++) {
_loop(i);
}
}
// list of properties that are of no interest when it comes to nodes and links comparison
var NODE_PROPERTIES_DISCARD_TO_COMPARE = ["x", "y", "vx", "vy", "index"];
/**
* This function checks for graph elements (nodes and links) changes, in two different
* levels of significance, updated elements (whether some property has changed in some
* node or link) and new elements (whether some new elements or added/removed from the graph).
* @param {Object} nextProps - nextProps that graph will receive.
* @param {Object} currentState - the current state of the graph.
* @returns {Object.<string, boolean>} returns object containing update check flags:
* - newGraphElements - flag that indicates whether new graph elements were added.
* - graphElementsUpdated - flag that indicates whether some graph elements have
* updated (some property that is not in NODE_PROPERTIES_DISCARD_TO_COMPARE was added to
* some node or link or was updated).
* @memberof Graph/helper
*/
function checkForGraphElementsChanges(nextProps, currentState) {
var nextNodes = nextProps.data.nodes.map(function (n) {
return _utils2.default.antiPick(n, NODE_PROPERTIES_DISCARD_TO_COMPARE);
});
var nextLinks = nextProps.data.links;
var stateD3Nodes = currentState.d3Nodes.map(function (n) {
return _utils2.default.antiPick(n, NODE_PROPERTIES_DISCARD_TO_COMPARE);
});
var stateD3Links = currentState.d3Links.map(function (l) {
return {
// FIXME: solve this source data inconsistency later
source: l.source.id !== undefined && l.source.id !== null ? l.source.id : l.source,
target: l.target.id !== undefined && l.target.id !== null ? l.target.id : l.target
};
});
var graphElementsUpdated = !(_utils2.default.isDeepEqual(nextNodes, stateD3Nodes) && _utils2.default.isDeepEqual(nextLinks, stateD3Links));
var newGraphElements = nextNodes.length !== stateD3Nodes.length || nextLinks.length !== stateD3Links.length || !_utils2.default.isDeepEqual(nextNodes.map(function (_ref) {
var id = _ref.id;
return { id: id };
}), stateD3Nodes.map(function (_ref2) {
var id = _ref2.id;
return { id: id };
})) || !_utils2.default.isDeepEqual(nextLinks, stateD3Links.map(function (_ref3) {
var source = _ref3.source,
target = _ref3.target;
return { source: source, target: target };
}));
return { graphElementsUpdated: graphElementsUpdated, newGraphElements: newGraphElements };
}
/**
* Logic to check for changes in graph config.
* @param {Object} nextProps - nextProps that graph will receive.
* @param {Object} currentState - the current state of the graph.
* @returns {Object.<string, boolean>} returns object containing update check flags:
* - configUpdated - global flag that indicates if any property was updated.
* - d3ConfigUpdated - specific flag that indicates changes in d3 configurations.
* @memberof Graph/helper
*/
function checkForGraphConfigChanges(nextProps, currentState) {
var newConfig = nextProps.config || {};
var configUpdated = newConfig && !_utils2.default.isEmptyObject(newConfig) && !_utils2.default.isDeepEqual(newConfig, currentState.config);
var d3ConfigUpdated = newConfig && newConfig.d3 && !_utils2.default.isDeepEqual(newConfig.d3, currentState.config.d3);
return { configUpdated: configUpdated, d3ConfigUpdated: d3ConfigUpdated };
}
/**
* Returns the transformation to apply in order to center the graph on the
* selected node.
* @param {Object} d3Node - node to focus the graph view on.
* @param {Object} config - same as {@link #graphrenderer|config in renderGraph}.
* @returns {string} transform rule to apply.
* @memberof Graph/helper
*/
function getCenterAndZoomTransformation(d3Node, config) {
if (!d3Node) {
return;
}
var width = config.width,
height = config.height,
focusZoom = config.focusZoom;
return "\n translate(" + width / 2 + ", " + height / 2 + ")\n scale(" + focusZoom + ")\n translate(" + -d3Node.x + ", " + -d3Node.y + ")\n ";
}
/**
* Encapsulates common procedures to initialize graph.
* @param {Object} props - Graph component props, object that holds data, id and config.
* @param {Object} props.data - Data object holds links (array of **Link**) and nodes (array of **Node**).
* @param {string} props.id - the graph id.
* @param {Object} props.config - same as {@link #graphrenderer|config in renderGraph}.
* @param {Object} state - Graph component current state (same format as returned object on this function).
* @returns {Object} a fully (re)initialized graph state object.
* @memberof Graph/helper
*/
function initializeGraphState(_ref4, state) {
var data = _ref4.data,
id = _ref4.id,
config = _ref4.config;
_validateGraphData(data);
var graph = void 0;
if (state && state.nodes) {
graph = {
nodes: data.nodes.map(function (n) {
return state.nodes[n.id] ? Object.assign({}, n, _utils2.default.pick(state.nodes[n.id], NODE_PROPS_WHITELIST)) : Object.assign({}, n);
}),
links: data.links.map(function (l, index) {
return _mapDataLinkToD3Link(l, index, state && state.d3Links, config, state);
})
};
} else {
graph = {
nodes: data.nodes.map(function (n) {
return Object.assign({}, n);
}),
links: data.links.map(function (l) {
return Object.assign({}, l);
})
};
}
var newConfig = Object.assign({}, _utils2.default.merge(_graph5.default, config || {}));
var links = _initializeLinks(graph.links, newConfig); // matrix of graph connections
var nodes = _tagOrphanNodes(_initializeNodes(graph.nodes), links);
var _graph = graph,
d3Nodes = _graph.nodes,
d3Links = _graph.links;
var formatedId = id.replace(/ /g, "_");
var simulation = _createForceSimulation(newConfig.width, newConfig.height, newConfig.d3 && newConfig.d3.gravity);
var minZoom = newConfig.minZoom,
maxZoom = newConfig.maxZoom,
focusZoom = newConfig.focusZoom;
if (focusZoom > maxZoom) {
newConfig.focusZoom = maxZoom;
} else if (focusZoom < minZoom) {
newConfig.focusZoom = minZoom;
}
return {
id: formatedId,
config: newConfig,
links: links,
d3Links: d3Links,
nodes: nodes,
d3Nodes: d3Nodes,
highlightedNode: "",
simulation: simulation,
newGraphElements: false,
configUpdated: false,
transform: 1
};
}
/**
* This function updates the highlighted value for a given node and also updates highlight props.
* @param {Object.<string, Object>} nodes - an object containing all nodes mapped by their id.
* @param {Object.<string, Object>} links - an object containing a matrix of connections of the graph.
* @param {Object} config - an object containing rd3g consumer defined configurations {@link #config config} for the graph.
* @param {string} id - identifier of node to update.
* @param {string} value - new highlight value for given node.
* @returns {Object} returns an object containing the updated nodes
* and the id of the highlighted node.
* @memberof Graph/helper
*/
function updateNodeHighlightedValue(nodes, links, config, id) {
var value = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
var highlightedNode = value ? id : "";
var node = Object.assign({}, nodes[id], { highlighted: value });
var updatedNodes = Object.assign({}, nodes, _defineProperty({}, id, node));
// when highlightDegree is 0 we want only to highlight selected node
if (links[id] && config.highlightDegree !== 0) {
updatedNodes = Object.keys(links[id]).reduce(function (acc, linkId) {
var updatedNode = Object.assign({}, updatedNodes[linkId], { highlighted: value });
return Object.assign(acc, _defineProperty({}, linkId, updatedNode));
}, updatedNodes);
}
return {
nodes: updatedNodes,
highlightedNode: highlightedNode
};
}
exports.checkForGraphConfigChanges = checkForGraphConfigChanges;
exports.checkForGraphElementsChanges = checkForGraphElementsChanges;
exports.getCenterAndZoomTransformation = getCenterAndZoomTransformation;
exports.initializeGraphState = initializeGraphState;
exports.updateNodeHighlightedValue = updateNodeHighlightedValue;