From ac066aee2bc14f0011f902257d289af191d282b1 Mon Sep 17 00:00:00 2001 From: danielcaldas Date: Sat, 9 Dec 2017 23:47:45 +0000 Subject: [PATCH 1/8] Separate graph.helper into helper.js and renderer.jsx --- .gitignore | 3 +- src/components/graph/graph.helper.js | 353 ++++++++++++++++++ src/components/graph/graph.renderer.jsx | 129 +++++++ src/components/graph/helper.jsx | 467 ------------------------ src/components/graph/index.jsx | 5 +- 5 files changed, 487 insertions(+), 470 deletions(-) create mode 100644 src/components/graph/graph.helper.js create mode 100644 src/components/graph/graph.renderer.jsx delete mode 100644 src/components/graph/helper.jsx diff --git a/.gitignore b/.gitignore index d1dda9fcb..568442029 100644 --- a/.gitignore +++ b/.gitignore @@ -70,4 +70,5 @@ coverage sandbox/rd3g.sandbox.bundle.js.map gen-docs .DS_Store -.vscode/ +**/.DS_Store +.vscode diff --git a/src/components/graph/graph.helper.js b/src/components/graph/graph.helper.js new file mode 100644 index 000000000..b1f10d770 --- /dev/null +++ b/src/components/graph/graph.helper.js @@ -0,0 +1,353 @@ +/** + * @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. + * @memberof Graph/helper + */ +import { + forceX as d3ForceX, + forceY as d3ForceY, + forceSimulation as d3ForceSimulation, + forceManyBody as d3ForceManyBody +} from 'd3-force'; + +import CONST from './const'; +import DEFAULT_CONFIG from './config'; +import ERRORS from '../../err'; + +import utils from '../../utils'; + +/** + * Create d3 forceSimulation to be applied on the graph.
+ * {@link https://github.com/d3/d3-force#forceSimulation|d3-force#forceSimulation}
+ * {@link https://github.com/d3/d3-force#simulation_force|d3-force#simulation_force}
+ * 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. + * @returns {Object} returns the simulation instance to be consumed. + * @memberof Graph/helper + */ +function _createForceSimulation(width, height) { + const frx = d3ForceX(width / 2).strength(CONST.FORCE_X); + const fry = d3ForceY(height / 2).strength(CONST.FORCE_Y); + + return d3ForceSimulation() + .force('charge', d3ForceManyBody().strength(CONST.FORCE_IDEAL_STRENGTH)) + .force('x', frx) + .force('y', fry); +} + +/** +* Get the correct node opacity in order to properly make decisions based on context such as currently highlighted node. +* @param {Object} node - the node object for whom we will generate properties. +* @param {string} highlightedNode - same as {@link #buildGraph|highlightedNode in buildGraph}. +* @param {Object} highlightedLink - same as {@link #buildGraph|highlightedLink in buildGraph}. +* @param {Object} config - same as {@link #buildGraph|config in buildGraph}. +* @returns {number} the opacity value for the given node. +* @memberof Graph/helper +*/ +function _getNodeOpacity(node, highlightedNode, highlightedLink, config) { + const highlight = node.highlighted + || node.id === (highlightedLink && highlightedLink.source) + || node.id === (highlightedLink && highlightedLink.target); + const someNodeHighlighted = !!(highlightedNode + || highlightedLink && highlightedLink.source && highlightedLink.target); + let opacity; + + if (someNodeHighlighted && config.highlightDegree === 0) { + opacity = highlight ? config.node.opacity : config.highlightOpacity; + } else if (someNodeHighlighted) { + opacity = highlight ? config.node.opacity : config.highlightOpacity; + } else { + opacity = config.node.opacity; + } + + return opacity; +} + +/** +* 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 {Object[]} graphLinks - an array of all graph links but all the links contain the source and target nodes +* objects. +* @returns {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) { + return graphLinks.reduce((links, l) => { + const source = l.source.id || l.source; + const target = l.target.id || l.target; + + if (!links[source]) { + links[source] = {}; + } + + if (!links[target]) { + links[target] = {}; + } + + // @TODO: If the graph is directed this should be adapted + links[source][target] = links[target][source] = l.value || 1; + + 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 {Object[]} graphNodes - the array of nodes provided by the rd3g consumer. +* @returns {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) { + let nodes = {}; + const n = graphNodes.length; + + for (let i=0; i < n; i++) { + const 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; +} + +/** +* 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}. +* @memberof Graph/helper +* @throws can throw the following error msg: +* INSUFFICIENT_DATA - msg if no nodes are provided +* INVALID_LINKS - if links point to nonexistent nodes +*/ +function _validateGraphData(data) { + if (!data.nodes || !data.nodes.length) { + utils.throwErr('Graph', ERRORS.INSUFFICIENT_DATA); + } + + const n = data.links.length; + + for (let i=0; i < n; i++) { + const l = data.links[i]; + + if (!data.nodes.find(n => n.id === l.source)) { + utils.throwErr('Graph', `${ERRORS.INVALID_LINKS} - "${l.source}" is not a valid source node id`); + } + if (!data.nodes.find(n => n.id === l.target)) { + utils.throwErr('Graph', `${ERRORS.INVALID_LINKS} - "${l.target}" is not a valid target node id`); + } + } +} + +/** + * Build some Link properties based on given parameters. + * @param {string} source - the id of the source node (from). + * @param {string} target - the id of the target node (to). + * @param {Object.} nodes - same as {@link #buildGraph|nodes in buildGraph}. + * @param {Object.} links - same as {@link #buildGraph|links in buildGraph}. + * @param {Object} config - same as {@link #buildGraph|config in buildGraph}. + * @param {Function[]} linkCallbacks - same as {@link #buildGraph|linkCallbacks in buildGraph}. + * @param {string} highlightedNode - same as {@link #buildGraph|highlightedNode in buildGraph}. + * @param {Object} highlightedLink - same as {@link #buildGraph|highlightedLink in buildGraph}. + * @param {number} transform - value that indicates the amount of zoom transformation. + * @returns {Object} returns an object that aggregates all props for creating respective Link component instance. + * @memberof Graph/helper + */ +function buildLinkProps(source, target, nodes, links, config, linkCallbacks, highlightedNode, + highlightedLink, transform) { + const x1 = nodes[source] && nodes[source].x || 0; + const y1 = nodes[source] && nodes[source].y || 0; + const x2 = nodes[target] && nodes[target].x || 0; + const y2 = nodes[target] && nodes[target].y || 0; + + let mainNodeParticipates = false; + + switch (config.highlightDegree) { + case 0: + break; + case 2: + mainNodeParticipates = true; + break; + default: // 1st degree is the fallback behavior + mainNodeParticipates = source === highlightedNode || target === highlightedNode; + break; + } + + const reasonNode = mainNodeParticipates && nodes[source].highlighted && nodes[target].highlighted; + const reasonLink = source === (highlightedLink && highlightedLink.source) + && target === (highlightedLink && highlightedLink.target); + const highlight = reasonNode || reasonLink; + + let opacity = config.link.opacity; + + if (highlightedNode || (highlightedLink && highlightedLink.source)) { + opacity = highlight ? config.link.opacity : config.highlightOpacity; + } + + let stroke = config.link.color; + + if (highlight) { + stroke = config.link.highlightColor === CONST.KEYWORDS.SAME ? config.link.color + : config.link.highlightColor; + } + + let strokeWidth = config.link.strokeWidth * (1 / transform); + + if (config.link.semanticStrokeWidth) { + const linkValue = links[source][target] || links[target][source] || 1; + + strokeWidth += (linkValue * strokeWidth) / 10; + } + + return { + source, + target, + x1, + y1, + x2, + y2, + strokeWidth, + stroke, + className: CONST.LINK_CLASS_NAME, + opacity, + onClickLink: linkCallbacks.onClickLink, + onMouseOverLink: linkCallbacks.onMouseOverLink, + onMouseOutLink: linkCallbacks.onMouseOutLink + }; +} + +/** +* Build some Node properties based on given parameters. +* @param {Object} node - the node object for whom we will generate properties. +* @param {Object} config - same as {@link #buildGraph|config in buildGraph}. +* @param {Function[]} nodeCallbacks - same as {@link #buildGraph|nodeCallbacks in buildGraph}. +* @param {string} highlightedNode - same as {@link #buildGraph|highlightedNode in buildGraph}. +* @param {Object} highlightedLink - same as {@link #buildGraph|highlightedLink in buildGraph}. +* @param {number} transform - value that indicates the amount of zoom transformation. +* @returns {Object} returns object that contain Link props ready to be feeded to the Link component. +* @memberof Graph/helper +*/ +function buildNodeProps(node, config, nodeCallbacks, highlightedNode, highlightedLink, transform) { + const highlight = node.highlighted + || (node.id === (highlightedLink && highlightedLink.source) + || node.id === (highlightedLink && highlightedLink.target)); + const opacity = _getNodeOpacity(node, highlightedNode, highlightedLink, config); + let fill = node.color || config.node.color; + + if (highlight && config.node.highlightColor !== CONST.KEYWORDS.SAME) { + fill = config.node.highlightColor; + } + + let stroke = config.node.strokeColor; + + if (highlight && config.node.highlightStrokeColor !== CONST.KEYWORDS.SAME) { + stroke = config.node.highlightStrokeColor; + } + + const t = 1 / transform; + const nodeSize = node.size || config.node.size; + const fontSize = highlight ? config.node.highlightFontSize : config.node.fontSize; + const dx = (fontSize * t) + (nodeSize / 100) + 1.5; + const strokeWidth = highlight ? config.node.highlightStrokeWidth : config.node.strokeWidth; + + return { + className: CONST.NODE_CLASS_NAME, + cursor: config.node.mouseCursor, + cx: node && node.x || '0', + cy: node && node.y || '0', + fill, + fontSize: fontSize * t, + dx, + fontWeight: highlight ? config.node.highlightFontWeight : config.node.fontWeight, + id: node.id, + label: node[config.node.labelProperty] || node.id, + onClickNode: nodeCallbacks.onClickNode, + onMouseOverNode: nodeCallbacks.onMouseOverNode, + onMouseOut: nodeCallbacks.onMouseOut, + opacity, + renderLabel: config.node.renderLabel, + size: nodeSize * t, + stroke, + strokeWidth: strokeWidth * t, + type: node.symbolType || config.node.symbolType + }; +} + +/** + * 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 #buildGraph|config in buildGraph}. + * @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({data, id, config}, state) { + let graph; + + _validateGraphData(data); + + if (state && state.nodes && state.links) { + // absorb existent positioning + graph = { + nodes: data.nodes.map(n => Object.assign({}, n, state.nodes[n.id])), + links: {} + }; + } else { + graph = { + nodes: data.nodes.map(n => Object.assign({}, n)), + links: {} + }; + } + + graph.links = data.links.map(l => Object.assign({}, l)); + + let newConfig = Object.assign({}, utils.merge(DEFAULT_CONFIG, config || {})); + let nodes = _initializeNodes(graph.nodes); + let links = _initializeLinks(graph.links); // matrix of graph connections + const {nodes: d3Nodes, links: d3Links} = graph; + const formatedId = id.replace(/ /g, '_'); + const simulation = _createForceSimulation(newConfig.width, newConfig.height); + + return { + id: formatedId, + config: newConfig, + links, + d3Links, + nodes, + d3Nodes, + highlightedNode: '', + simulation, + newGraphElements: false, + configUpdated: false, + transform: 1 + }; +} + +export { + buildLinkProps, + buildNodeProps, + initializeGraphState +}; \ No newline at end of file diff --git a/src/components/graph/graph.renderer.jsx b/src/components/graph/graph.renderer.jsx new file mode 100644 index 000000000..6c020991d --- /dev/null +++ b/src/components/graph/graph.renderer.jsx @@ -0,0 +1,129 @@ +/** + * @module Graph/renderer + * @description + * Offers a series of methods that isolate render logic for Graph component. + */ +import React from 'react'; + +import CONST from './const'; + +import Link from '../link/'; +import Node from '../node/'; +import { + buildLinkProps, + buildNodeProps +} from './graph.helper'; + +/** + * Build Link components for a given node. + * @param {string} nodeId - the id of the node to whom Link components will be generated. + * @param {Object.} nodes - same as {@link #buildGraph|nodes in buildGraph}. + * @param {Object.} links - same as {@link #buildGraph|links in buildGraph}. + * @param {Object} config - same as {@link #buildGraph|config in buildGraph}. + * @param {Function[]} linkCallbacks - same as {@link #buildGraph|linkCallbacks in buildGraph}. + * @param {string} highlightedNode - same as {@link #buildGraph|highlightedNode in buildGraph}. + * @param {Object} highlightedLink - same as {@link #buildGraph|highlightedLink in buildGraph}. + * @param {number} transform - value that indicates the amount of zoom transformation. + * @returns {Object[]} returns the generated array of Link components. + * @memberof Graph/helper + */ +function _buildNodeLinks(nodeId, nodes, links, config, linkCallbacks, highlightedNode, highlightedLink, transform) { + let linksComponents = []; + + if (links[nodeId]) { + const adjacents = Object.keys(links[nodeId]); + const n = adjacents.length; + + for (let j=0; j < n; j++) { + const source = nodeId; + const target = adjacents[j]; + + if (nodes[target]) { + const key = `${nodeId}${CONST.COORDS_SEPARATOR}${target}`; + const props = buildLinkProps( + source, + target, + nodes, + links, + config, + linkCallbacks, + highlightedNode, + highlightedLink, + transform + ); + + linksComponents.push(); + } + } + } + + return linksComponents; +} + +/** + * Method that actually is exported an consumed by Graph component in order to build all Nodes and Link + * components. + * @param {Object.} nodes - an object containing all nodes mapped by their id. + * @param {Function[]} nodeCallbacks - array of callbacks for used defined event handler for node interactions. + * @param {Object.} links - 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). + * ```javascript + * // links example + * { + * "Androsynth": { + * "Chenjesu": 1, + * "Ilwrath": 1, + * "Mycon": 1, + * "Spathi": 1, + * "Umgah": 1, + * "VUX": 1, + * "Guardian": 1 + * }, + * "Chenjesu": { + * "Androsynth": 1, + * "Mycon": 1, + * "Spathi": 1, + * "Umgah": 1, + * "VUX": 1, + * "Broodhmome": 1 + * }, + * ... + * } + * ``` + * @param {Function[]} linkCallbacks - array of callbacks for used defined event handler for link interactions. + * @param {Object} config - an object containing rd3g consumer defined configurations {@link #config config} for the graph. + * @param {string} highlightedNode - this value contains a string that represents the some currently highlighted node. + * @param {Object} highlightedLink - this object contains a source and target property for a link that is highlighted at some point in time. + * @param {string} highlightedLink.source - id of source node for highlighted link. + * @param {string} highlightedLink.target - id of target node for highlighted link. + * @param {number} transform - value that indicates the amount of zoom transformation. + * @returns {Object} returns an object containing the generated nodes and links that form the graph. The result is + * returned in a way that can be consumed by es6 **destructuring assignment**. + * @memberof Graph/helper + */ +function buildGraph(nodes, nodeCallbacks, links, linkCallbacks, config, highlightedNode, highlightedLink, transform) { + let linksComponents = []; + let nodesComponents = []; + + for (let i = 0, keys = Object.keys(nodes), n = keys.length; i < n; i++) { + const nodeId = keys[i]; + + const props = buildNodeProps(nodes[nodeId], config, nodeCallbacks, + highlightedNode, highlightedLink, transform); + + nodesComponents.push(); + + linksComponents = linksComponents.concat( + _buildNodeLinks(nodeId, nodes, links, config, linkCallbacks, highlightedNode, highlightedLink, transform) + ); + } + + return { + nodes: nodesComponents, + links: linksComponents + }; +} + +export { + buildGraph +}; diff --git a/src/components/graph/helper.jsx b/src/components/graph/helper.jsx deleted file mode 100644 index f54c00eee..000000000 --- a/src/components/graph/helper.jsx +++ /dev/null @@ -1,467 +0,0 @@ -/** - * @module Graph/helper - * @description - * Offers a series of methods that isolate logic of Graph component. - */ -/** - * @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. - * @memberof Graph/helper - */ -import React from 'react'; - -import { - forceX as d3ForceX, - forceY as d3ForceY, - forceSimulation as d3ForceSimulation, - forceManyBody as d3ForceManyBody -} from 'd3-force'; - -import CONST from './const'; -import DEFAULT_CONFIG from './config'; -import ERRORS from '../../err'; - -import Link from '../link/'; -import Node from '../node/'; -import utils from '../../utils'; - -/** - * Build some Link properties based on given parameters. - * @param {string} source - the id of the source node (from). - * @param {string} target - the id of the target node (to). - * @param {Object.} nodes - same as {@link #buildGraph|nodes in buildGraph}. - * @param {Object.} links - same as {@link #buildGraph|links in buildGraph}. - * @param {Object} config - same as {@link #buildGraph|config in buildGraph}. - * @param {Function[]} linkCallbacks - same as {@link #buildGraph|linkCallbacks in buildGraph}. - * @param {string} highlightedNode - same as {@link #buildGraph|highlightedNode in buildGraph}. - * @param {Object} highlightedLink - same as {@link #buildGraph|highlightedLink in buildGraph}. - * @param {number} transform - value that indicates the amount of zoom transformation. - * @returns {Object} returns an object that aggregates all props for creating respective Link component instance. - * @memberof Graph/helper - */ -function _buildLinkProps(source, target, nodes, links, config, linkCallbacks, highlightedNode, - highlightedLink, transform) { - const x1 = nodes[source] && nodes[source].x || 0; - const y1 = nodes[source] && nodes[source].y || 0; - const x2 = nodes[target] && nodes[target].x || 0; - const y2 = nodes[target] && nodes[target].y || 0; - - let mainNodeParticipates = false; - - switch (config.highlightDegree) { - case 0: - break; - case 2: - mainNodeParticipates = true; - break; - default: // 1st degree is the fallback behavior - mainNodeParticipates = source === highlightedNode || target === highlightedNode; - break; - } - - const reasonNode = mainNodeParticipates && nodes[source].highlighted && nodes[target].highlighted; - const reasonLink = source === (highlightedLink && highlightedLink.source) - && target === (highlightedLink && highlightedLink.target); - const highlight = reasonNode || reasonLink; - - let opacity = config.link.opacity; - - if (highlightedNode || (highlightedLink && highlightedLink.source)) { - opacity = highlight ? config.link.opacity : config.highlightOpacity; - } - - let stroke = config.link.color; - - if (highlight) { - stroke = config.link.highlightColor === CONST.KEYWORDS.SAME ? config.link.color - : config.link.highlightColor; - } - - let strokeWidth = config.link.strokeWidth * (1 / transform); - - if (config.link.semanticStrokeWidth) { - const linkValue = links[source][target] || links[target][source] || 1; - - strokeWidth += (linkValue * strokeWidth) / 10; - } - - return { - source, - target, - x1, - y1, - x2, - y2, - strokeWidth, - stroke, - className: CONST.LINK_CLASS_NAME, - opacity, - onClickLink: linkCallbacks.onClickLink, - onMouseOverLink: linkCallbacks.onMouseOverLink, - onMouseOutLink: linkCallbacks.onMouseOutLink - }; -} - -/** - * Build Link components for a given node. - * @param {string} nodeId - the id of the node to whom Link components will be generated. - * @param {Object.} nodes - same as {@link #buildGraph|nodes in buildGraph}. - * @param {Object.} links - same as {@link #buildGraph|links in buildGraph}. - * @param {Object} config - same as {@link #buildGraph|config in buildGraph}. - * @param {Function[]} linkCallbacks - same as {@link #buildGraph|linkCallbacks in buildGraph}. - * @param {string} highlightedNode - same as {@link #buildGraph|highlightedNode in buildGraph}. - * @param {Object} highlightedLink - same as {@link #buildGraph|highlightedLink in buildGraph}. - * @param {number} transform - value that indicates the amount of zoom transformation. - * @returns {Object[]} returns the generated array of Link components. - * @memberof Graph/helper - */ -function _buildNodeLinks(nodeId, nodes, links, config, linkCallbacks, highlightedNode, highlightedLink, transform) { - let linksComponents = []; - - if (links[nodeId]) { - const adjacents = Object.keys(links[nodeId]); - const n = adjacents.length; - - for (let j=0; j < n; j++) { - const source = nodeId; - const target = adjacents[j]; - - if (nodes[target]) { - const key = `${nodeId}${CONST.COORDS_SEPARATOR}${target}`; - const props = _buildLinkProps( - source, - target, - nodes, - links, - config, - linkCallbacks, - highlightedNode, - highlightedLink, - transform - ); - - linksComponents.push(); - } - } - } - - return linksComponents; -} - -/** - * Build some Node properties based on given parameters. - * @param {Object} node - the node object for whom we will generate properties. - * @param {Object} config - same as {@link #buildGraph|config in buildGraph}. - * @param {Function[]} nodeCallbacks - same as {@link #buildGraph|nodeCallbacks in buildGraph}. - * @param {string} highlightedNode - same as {@link #buildGraph|highlightedNode in buildGraph}. - * @param {Object} highlightedLink - same as {@link #buildGraph|highlightedLink in buildGraph}. - * @param {number} transform - value that indicates the amount of zoom transformation. - * @returns {Object} returns object that contain Link props ready to be feeded to the Link component. - * @memberof Graph/helper - */ -function _buildNodeProps(node, config, nodeCallbacks, highlightedNode, highlightedLink, transform) { - const highlight = node.highlighted - || (node.id === (highlightedLink && highlightedLink.source) - || node.id === (highlightedLink && highlightedLink.target)); - const opacity = _getNodeOpacity(node, highlightedNode, highlightedLink, config); - let fill = node.color || config.node.color; - - if (highlight && config.node.highlightColor !== CONST.KEYWORDS.SAME) { - fill = config.node.highlightColor; - } - - let stroke = config.node.strokeColor; - - if (highlight && config.node.highlightStrokeColor !== CONST.KEYWORDS.SAME) { - stroke = config.node.highlightStrokeColor; - } - - const t = 1 / transform; - const nodeSize = node.size || config.node.size; - const fontSize = highlight ? config.node.highlightFontSize : config.node.fontSize; - const dx = (fontSize * t) + (nodeSize / 100) + 1.5; - const strokeWidth = highlight ? config.node.highlightStrokeWidth : config.node.strokeWidth; - - return { - className: CONST.NODE_CLASS_NAME, - cursor: config.node.mouseCursor, - cx: node && node.x || '0', - cy: node && node.y || '0', - fill, - fontSize: fontSize * t, - dx, - fontWeight: highlight ? config.node.highlightFontWeight : config.node.fontWeight, - id: node.id, - label: node[config.node.labelProperty] || node.id, - onClickNode: nodeCallbacks.onClickNode, - onMouseOverNode: nodeCallbacks.onMouseOverNode, - onMouseOut: nodeCallbacks.onMouseOut, - opacity, - renderLabel: config.node.renderLabel, - size: nodeSize * t, - stroke, - strokeWidth: strokeWidth * t, - type: node.symbolType || config.node.symbolType - }; -} - -/** - * Get the correct node opacity in order to properly make decisions based on context such as currently highlighted node. - * @param {Object} node - the node object for whom we will generate properties. - * @param {string} highlightedNode - same as {@link #buildGraph|highlightedNode in buildGraph}. - * @param {Object} highlightedLink - same as {@link #buildGraph|highlightedLink in buildGraph}. - * @param {Object} config - same as {@link #buildGraph|config in buildGraph}. - * @returns {number} the opacity value for the given node. - * @memberof Graph/helper - */ -function _getNodeOpacity(node, highlightedNode, highlightedLink, config) { - const highlight = node.highlighted - || node.id === (highlightedLink && highlightedLink.source) - || node.id === (highlightedLink && highlightedLink.target); - const someNodeHighlighted = !!(highlightedNode - || highlightedLink && highlightedLink.source && highlightedLink.target); - let opacity; - - if (someNodeHighlighted && config.highlightDegree === 0) { - opacity = highlight ? config.node.opacity : config.highlightOpacity; - } else if (someNodeHighlighted) { - opacity = highlight ? config.node.opacity : config.highlightOpacity; - } else { - opacity = config.node.opacity; - } - - return opacity; -} - -/** - * 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 {Object[]} graphLinks - an array of all graph links but all the links contain the source and target nodes - * objects. - * @returns {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) { - return graphLinks.reduce((links, l) => { - const source = l.source.id || l.source; - const target = l.target.id || l.target; - - if (!links[source]) { - links[source] = {}; - } - - if (!links[target]) { - links[target] = {}; - } - - // @TODO: If the graph is directed this should be adapted - links[source][target] = links[target][source] = l.value || 1; - - 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 {Object[]} graphNodes - the array of nodes provided by the rd3g consumer. - * @returns {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) { - let nodes = {}; - const n = graphNodes.length; - - for (let i=0; i < n; i++) { - const 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; -} - -/** - * 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}. - * @memberof Graph/helper - * @throws can throw the following error msg: - * INSUFFICIENT_DATA - msg if no nodes are provided - * INVALID_LINKS - if links point to nonexistent nodes - */ -function _validateGraphData(data) { - if (!data.nodes || !data.nodes.length) { - utils.throwErr('Graph', ERRORS.INSUFFICIENT_DATA); - } - - const n = data.links.length; - - for (let i=0; i < n; i++) { - const l = data.links[i]; - - if (!data.nodes.find(n => n.id === l.source)) { - utils.throwErr('Graph', `${ERRORS.INVALID_LINKS} - "${l.source}" is not a valid source node id`); - } - if (!data.nodes.find(n => n.id === l.target)) { - utils.throwErr('Graph', `${ERRORS.INVALID_LINKS} - "${l.target}" is not a valid target node id`); - } - } -} - -/** - * Method that actually is exported an consumed by Graph component in order to build all Nodes and Link - * components. - * @param {Object.} nodes - an object containing all nodes mapped by their id. - * @param {Function[]} nodeCallbacks - array of callbacks for used defined event handler for node interactions. - * @param {Object.} links - 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). - * ```javascript - * // links example - * { - * "Androsynth": { - * "Chenjesu": 1, - * "Ilwrath": 1, - * "Mycon": 1, - * "Spathi": 1, - * "Umgah": 1, - * "VUX": 1, - * "Guardian": 1 - * }, - * "Chenjesu": { - * "Androsynth": 1, - * "Mycon": 1, - * "Spathi": 1, - * "Umgah": 1, - * "VUX": 1, - * "Broodhmome": 1 - * }, - * ... - * } - * ``` - * @param {Function[]} linkCallbacks - array of callbacks for used defined event handler for link interactions. - * @param {Object} config - an object containing rd3g consumer defined configurations {@link #config config} for the graph. - * @param {string} highlightedNode - this value contains a string that represents the some currently highlighted node. - * @param {Object} highlightedLink - this object contains a source and target property for a link that is highlighted at some point in time. - * @param {string} highlightedLink.source - id of source node for highlighted link. - * @param {string} highlightedLink.target - id of target node for highlighted link. - * @param {number} transform - value that indicates the amount of zoom transformation. - * @returns {Object} returns an object containing the generated nodes and links that form the graph. The result is - * returned in a way that can be consumed by es6 **destructuring assignment**. - * @memberof Graph/helper - */ -function buildGraph(nodes, nodeCallbacks, links, linkCallbacks, config, highlightedNode, highlightedLink, transform) { - let linksComponents = []; - let nodesComponents = []; - - for (let i = 0, keys = Object.keys(nodes), n = keys.length; i < n; i++) { - const nodeId = keys[i]; - - const props = _buildNodeProps(nodes[nodeId], config, nodeCallbacks, - highlightedNode, highlightedLink, transform); - - nodesComponents.push(); - - linksComponents = linksComponents.concat( - _buildNodeLinks(nodeId, nodes, links, config, linkCallbacks, highlightedNode, highlightedLink, transform) - ); - } - - return { - nodes: nodesComponents, - links: linksComponents - }; -} - -/** - * Create d3 forceSimulation to be applied on the graph.
- * {@link https://github.com/d3/d3-force#forceSimulation|d3-force#forceSimulation}
- * {@link https://github.com/d3/d3-force#simulation_force|d3-force#simulation_force}
- * 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. - * @returns {Object} returns the simulation instance to be consumed. - * @memberof Graph/helper - */ -function createForceSimulation(width, height) { - const frx = d3ForceX(width / 2).strength(CONST.FORCE_X); - const fry = d3ForceY(height / 2).strength(CONST.FORCE_Y); - - return d3ForceSimulation() - .force('charge', d3ForceManyBody().strength(CONST.FORCE_IDEAL_STRENGTH)) - .force('x', frx) - .force('y', fry); -} - -/** - * 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 #buildGraph|config in buildGraph}. - * @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({data, id, config}, state) { - let graph; - - _validateGraphData(data); - - if (state && state.nodes && state.links) { - // absorb existent positioning - graph = { - nodes: data.nodes.map(n => Object.assign({}, n, state.nodes[n.id])), - links: {} - }; - } else { - graph = { - nodes: data.nodes.map(n => Object.assign({}, n)), - links: {} - }; - } - - graph.links = data.links.map(l => Object.assign({}, l)); - - let newConfig = Object.assign({}, utils.merge(DEFAULT_CONFIG, config || {})); - let nodes = _initializeNodes(graph.nodes); - let links = _initializeLinks(graph.links); // matrix of graph connections - const {nodes: d3Nodes, links: d3Links} = graph; - const formatedId = id.replace(/ /g, '_'); - const simulation = createForceSimulation(newConfig.width, newConfig.height); - - return { - id: formatedId, - config: newConfig, - links, - d3Links, - nodes, - d3Nodes, - highlightedNode: '', - simulation, - newGraphElements: false, - configUpdated: false, - transform: 1 - }; -} - -export default { - buildGraph, - createForceSimulation, - initializeGraphState -}; diff --git a/src/components/graph/index.jsx b/src/components/graph/index.jsx index ba1771c8b..9f64924e7 100644 --- a/src/components/graph/index.jsx +++ b/src/components/graph/index.jsx @@ -13,7 +13,8 @@ import CONST from './const'; import DEFAULT_CONFIG from './config'; import ERRORS from '../../err'; -import graphHelper from './helper'; +import * as graphRenderer from './graph.renderer'; +import * as graphHelper from './graph.helper'; import utils from '../../utils'; // Some d3 constant values @@ -350,7 +351,7 @@ export default class Graph extends React.Component { } render() { - const { nodes, links } = graphHelper.buildGraph( + const { nodes, links } = graphRenderer.buildGraph( this.state.nodes, { onClickNode: this.props.onClickNode, From 0e19aa768ffe395217562f1c499d0f339bc47d8a Mon Sep 17 00:00:00 2001 From: danielcaldas Date: Sat, 9 Dec 2017 23:51:15 +0000 Subject: [PATCH 2/8] Fix jsdoc format in graph.helper.js --- src/components/graph/graph.helper.js | 86 ++++++++++++++-------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/src/components/graph/graph.helper.js b/src/components/graph/graph.helper.js index b1f10d770..6c50cb67b 100644 --- a/src/components/graph/graph.helper.js +++ b/src/components/graph/graph.helper.js @@ -48,14 +48,14 @@ function _createForceSimulation(width, height) { } /** -* Get the correct node opacity in order to properly make decisions based on context such as currently highlighted node. -* @param {Object} node - the node object for whom we will generate properties. -* @param {string} highlightedNode - same as {@link #buildGraph|highlightedNode in buildGraph}. -* @param {Object} highlightedLink - same as {@link #buildGraph|highlightedLink in buildGraph}. -* @param {Object} config - same as {@link #buildGraph|config in buildGraph}. -* @returns {number} the opacity value for the given node. -* @memberof Graph/helper -*/ + * Get the correct node opacity in order to properly make decisions based on context such as currently highlighted node. + * @param {Object} node - the node object for whom we will generate properties. + * @param {string} highlightedNode - same as {@link #buildGraph|highlightedNode in buildGraph}. + * @param {Object} highlightedLink - same as {@link #buildGraph|highlightedLink in buildGraph}. + * @param {Object} config - same as {@link #buildGraph|config in buildGraph}. + * @returns {number} the opacity value for the given node. + * @memberof Graph/helper + */ function _getNodeOpacity(node, highlightedNode, highlightedLink, config) { const highlight = node.highlighted || node.id === (highlightedLink && highlightedLink.source) @@ -76,15 +76,15 @@ function _getNodeOpacity(node, highlightedNode, highlightedLink, config) { } /** -* 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 {Object[]} graphLinks - an array of all graph links but all the links contain the source and target nodes -* objects. -* @returns {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 -*/ + * 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 {Object[]} graphLinks - an array of all graph links but all the links contain the source and target nodes + * objects. + * @returns {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) { return graphLinks.reduce((links, l) => { const source = l.source.id || l.source; @@ -106,14 +106,14 @@ function _initializeLinks(graphLinks) { } /** -* 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 {Object[]} graphNodes - the array of nodes provided by the rd3g consumer. -* @returns {Object} returns the nodes ready to be used within rd3g with additional properties such as x, y -* and highlighted values. -* @memberof Graph/helper -*/ + * 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 {Object[]} graphNodes - the array of nodes provided by the rd3g consumer. + * @returns {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) { let nodes = {}; const n = graphNodes.length; @@ -133,14 +133,14 @@ function _initializeNodes(graphNodes) { } /** -* 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}. -* @memberof Graph/helper -* @throws can throw the following error msg: -* INSUFFICIENT_DATA - msg if no nodes are provided -* INVALID_LINKS - if links point to nonexistent nodes -*/ + * 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}. + * @memberof Graph/helper + * @throws can throw the following error msg: + * INSUFFICIENT_DATA - msg if no nodes are provided + * INVALID_LINKS - if links point to nonexistent nodes + */ function _validateGraphData(data) { if (!data.nodes || !data.nodes.length) { utils.throwErr('Graph', ERRORS.INSUFFICIENT_DATA); @@ -238,16 +238,16 @@ function buildLinkProps(source, target, nodes, links, config, linkCallbacks, hig } /** -* Build some Node properties based on given parameters. -* @param {Object} node - the node object for whom we will generate properties. -* @param {Object} config - same as {@link #buildGraph|config in buildGraph}. -* @param {Function[]} nodeCallbacks - same as {@link #buildGraph|nodeCallbacks in buildGraph}. -* @param {string} highlightedNode - same as {@link #buildGraph|highlightedNode in buildGraph}. -* @param {Object} highlightedLink - same as {@link #buildGraph|highlightedLink in buildGraph}. -* @param {number} transform - value that indicates the amount of zoom transformation. -* @returns {Object} returns object that contain Link props ready to be feeded to the Link component. -* @memberof Graph/helper -*/ + * Build some Node properties based on given parameters. + * @param {Object} node - the node object for whom we will generate properties. + * @param {Object} config - same as {@link #buildGraph|config in buildGraph}. + * @param {Function[]} nodeCallbacks - same as {@link #buildGraph|nodeCallbacks in buildGraph}. + * @param {string} highlightedNode - same as {@link #buildGraph|highlightedNode in buildGraph}. + * @param {Object} highlightedLink - same as {@link #buildGraph|highlightedLink in buildGraph}. + * @param {number} transform - value that indicates the amount of zoom transformation. + * @returns {Object} returns object that contain Link props ready to be feeded to the Link component. + * @memberof Graph/helper + */ function buildNodeProps(node, config, nodeCallbacks, highlightedNode, highlightedLink, transform) { const highlight = node.highlighted || (node.id === (highlightedLink && highlightedLink.source) From 909973217c70b372dda507c42b8a7e7fb3decab7 Mon Sep 17 00:00:00 2001 From: danielcaldas Date: Sun, 10 Dec 2017 00:07:13 +0000 Subject: [PATCH 3/8] Update tests to fit new architecture. Simplify d3 force mocks --- test/component/graph/graph.helper.test.js | 59 ++++------------------- 1 file changed, 10 insertions(+), 49 deletions(-) diff --git a/test/component/graph/graph.helper.test.js b/test/component/graph/graph.helper.test.js index f196324d4..d554da740 100644 --- a/test/component/graph/graph.helper.test.js +++ b/test/component/graph/graph.helper.test.js @@ -1,4 +1,4 @@ -import graphHelper from '../../../src/components/graph/helper'; +import * as graphHelper from '../../../src/components/graph/graph.helper'; jest.mock('../../../src/utils'); import utils from '../../../src/utils'; @@ -12,57 +12,18 @@ import { } from 'd3-force'; describe('Graph Helper', () => { - describe('#createForceSimulation', () => { - test('should properly create d3 simulation object', () => { - const fr = 10; - const forceStub = jest.fn(); - - d3ForceX.mockImplementation(() => { - return { - strength: () => fr - }; - }); - d3ForceY.mockImplementation(() => { - return { - strength: () => fr - }; - }); - d3ForceManyBody.mockImplementation(() => { - return { - strength: () => fr - }; - }); - forceStub.mockImplementation(() => { - return { - force: forceStub - }; - }); - d3ForceSimulation.mockImplementation(() => { - return { - force: forceStub - }; - }); - - graphHelper.createForceSimulation(1000, 1000); - - expect(d3ForceX).toHaveBeenCalledWith(500); - expect(d3ForceY).toHaveBeenCalledWith(500); - expect(d3ForceSimulation).toHaveBeenCalledWith(); - expect(forceStub).toHaveBeenCalledTimes(3); - expect(forceStub).toHaveBeenCalledWith('charge', fr); - expect(forceStub).toHaveBeenCalledWith('x', fr); - expect(forceStub).toHaveBeenLastCalledWith('y', fr); - }); - }); - describe('#initializeGraphState', () => { describe('when valid graph data is provided', () => { beforeEach(() => { - utils.merge.mockImplementation(() => { - return { - config: 'config' - }; - }); + const fr = 10; + const forceStub = jest.fn(); + + d3ForceX.mockImplementation(() => ({strength: () => fr})); + d3ForceY.mockImplementation(() => ({strength: () => fr})); + d3ForceManyBody.mockImplementation(() => ({strength: () => fr})); + forceStub.mockImplementation(() => ({force: forceStub})); + d3ForceSimulation.mockImplementation(() => ({force: forceStub})); + utils.merge.mockImplementation(() => ({config: 'config'})); }); describe('and received state was already initialized', () => { From 55e15ac318ed60947fa292c69d42c7615b9d4eb7 Mon Sep 17 00:00:00 2001 From: danielcaldas Date: Sun, 10 Dec 2017 00:49:59 +0000 Subject: [PATCH 4/8] Specify some tests for graph helper buildNodeProps method --- test/component/graph/graph.helper.test.js | 54 +++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/test/component/graph/graph.helper.test.js b/test/component/graph/graph.helper.test.js index d554da740..153d2e74f 100644 --- a/test/component/graph/graph.helper.test.js +++ b/test/component/graph/graph.helper.test.js @@ -1,5 +1,7 @@ import * as graphHelper from '../../../src/components/graph/graph.helper'; +import config from '../../../src/components/graph/config'; + jest.mock('../../../src/utils'); import utils from '../../../src/utils'; @@ -12,6 +14,58 @@ import { } from 'd3-force'; describe('Graph Helper', () => { + describe('#buildNodeProps', () => { + let that = {}; + + beforeEach(() => { + const nodeConfig = Object.assign({}, config.node); + const linkConfig = Object.assign({}, config.link); + + that = { + config: { node: nodeConfig, link: linkConfig }, + node: { + id: 'id', + x: 1, + y: 2, + color: 'green', + highlighted: false, + symbolType: undefined + } + }; + }); + describe('when node to build is the highlightedNode', () => { + + }); + describe('when node to build is the highlightedLink target (or source)', () => { + describe('and highlight degree is 0', () => { + + }); + describe('and highlight degree is bigger then 0', () => { + + }); + }); + describe('when node to build is neither highlightedNode neither highlightedLink source/target', () => { + beforeEach(() => { + // set common node structure so that below we only override small stuff + }); + describe('and config.node.highlightColor is "SAME"', () => { + + }); + describe('and config.node.highlightStrokeColor is "SAME"', () => { + + }); + describe('and config.node.labelProperty some label property (not id)', () => { + + }); + describe('and node has a self (node level) color', () => { + + }); + describe('and node has a self (node level) symbolType', () => { + + }); + }); + }); + describe('#initializeGraphState', () => { describe('when valid graph data is provided', () => { beforeEach(() => { From 89293440ced9b107f87ef0288e70c26bf7cc992f Mon Sep 17 00:00:00 2001 From: danielcaldas Date: Tue, 26 Dec 2017 17:20:31 +0000 Subject: [PATCH 5/8] Add trello board badge to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6a6083c56..d6dbe69da 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# react-d3-graph · [![Build Status](https://travis-ci.org/danielcaldas/react-d3-graph.svg?branch=master)](https://travis-ci.org/danielcaldas/react-d3-graph) [![npm version](https://img.shields.io/badge/npm-v1.0.0-blue.svg)](https://www.npmjs.com/package/react-d3-graph) [![npm stats](https://img.shields.io/badge/downloads-1.5k-brightgreen.svg)](https://npm-stat.com/charts.html?package=react-d3-graph&from=2017-04-25&to=2017-12-26) [![probot enabled](https://img.shields.io/badge/probot:stale-enabled-yellow.svg)](https://probot.github.io/) +# react-d3-graph · [![Build Status](https://travis-ci.org/danielcaldas/react-d3-graph.svg?branch=master)](https://travis-ci.org/danielcaldas/react-d3-graph) [![npm version](https://img.shields.io/badge/npm-v1.0.0-blue.svg)](https://www.npmjs.com/package/react-d3-graph) [![npm stats](https://img.shields.io/badge/downloads-1.5k-brightgreen.svg)](https://npm-stat.com/charts.html?package=react-d3-graph&from=2017-04-25&to=2017-12-26) [![probot enabled](https://img.shields.io/badge/probot:stale-enabled-yellow.svg)](https://probot.github.io/) [![trello](https://img.shields.io/badge/trello-board-blue.svg)](https://trello.com/b/KrnmFXha/react-d3-graph) :book: [1.0.0](https://danielcaldas.github.io/react-d3-graph/docs/index.html) | [0.4.0](https://danielcaldas.github.io/react-d3-graph/docs/0.4.0.html) | [0.3.0](https://danielcaldas.github.io/react-d3-graph/docs/0.3.0.html) ### *Interactive and configurable graphs with react and d3 effortlessly* From 99bd2db684bfd448f67063cc1fa61da4c2c447a3 Mon Sep 17 00:00:00 2001 From: danielcaldas Date: Tue, 26 Dec 2017 17:37:43 +0000 Subject: [PATCH 6/8] Some improvements in graph.helper jsdoc --- src/components/graph/graph.helper.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/graph/graph.helper.js b/src/components/graph/graph.helper.js index 6c50cb67b..106c4ee14 100644 --- a/src/components/graph/graph.helper.js +++ b/src/components/graph/graph.helper.js @@ -12,6 +12,9 @@ /** * @typedef {Object} Node * @property {string} id - the id of the node. + * @property {string} [color] - color of the node (optional). + * @property {string} [size] - size of the node (optional). + * @property {string} [symbolType] - symbol type of the node (optional). * @memberof Graph/helper */ import { @@ -79,8 +82,7 @@ function _getNodeOpacity(node, highlightedNode, highlightedLink, config) { * 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 {Object[]} graphLinks - an array of all graph links but all the links contain the source and target nodes - * objects. + * @param {Array.} graphLinks - an array of all graph links. * @returns {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 @@ -109,8 +111,8 @@ function _initializeLinks(graphLinks) { * 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 {Object[]} graphNodes - the array of nodes provided by the rd3g consumer. - * @returns {Object} returns the nodes ready to be used within rd3g with additional properties such as x, y + * @param {Array.} graphNodes - the array of nodes provided by the rd3g consumer. + * @returns {Object.} returns the nodes ready to be used within rd3g with additional properties such as x, y * and highlighted values. * @memberof Graph/helper */ From 9e468ead832e901649c409fee181a6c58677f698 Mon Sep 17 00:00:00 2001 From: danielcaldas Date: Mon, 29 Jan 2018 23:04:47 +0000 Subject: [PATCH 7/8] Add tests for buildNodeProps in graph helper --- src/components/graph/graph.helper.js | 2 +- test/component/graph/graph.helper.test.js | 116 +++++++++++++++++----- 2 files changed, 94 insertions(+), 24 deletions(-) diff --git a/src/components/graph/graph.helper.js b/src/components/graph/graph.helper.js index 106c4ee14..a54ec6f9b 100644 --- a/src/components/graph/graph.helper.js +++ b/src/components/graph/graph.helper.js @@ -250,7 +250,7 @@ function buildLinkProps(source, target, nodes, links, config, linkCallbacks, hig * @returns {Object} returns object that contain Link props ready to be feeded to the Link component. * @memberof Graph/helper */ -function buildNodeProps(node, config, nodeCallbacks, highlightedNode, highlightedLink, transform) { +function buildNodeProps(node, config, nodeCallbacks={}, highlightedNode, highlightedLink, transform) { const highlight = node.highlighted || (node.id === (highlightedLink && highlightedLink.source) || node.id === (highlightedLink && highlightedLink.target)); diff --git a/test/component/graph/graph.helper.test.js b/test/component/graph/graph.helper.test.js index 153d2e74f..d7cf49b2d 100644 --- a/test/component/graph/graph.helper.test.js +++ b/test/component/graph/graph.helper.test.js @@ -34,34 +34,104 @@ describe('Graph Helper', () => { }; }); describe('when node to build is the highlightedNode', () => { - + test('should return node props with proper highlight values', () => { + that.node.highlighted = true; + Object.assign(that.config.node, { + highlightColor: 'red', + highlightFontSize: 12, + highlightFontWeight: 'bold', + highlightStrokeColor: 'yellow', + highlightStrokeWidth: 2 + }); + const props = graphHelper.buildNodeProps(that.node, that.config, undefined, 'id', undefined, 1); + + expect(props).toEqual({ + className: 'node', + cursor: 'pointer', + cx: 1, + cy: 2, + dx: 15.5, + fill: 'red', + fontSize: 12, + fontWeight: 'bold', + id: 'id', + label: 'id', + onClickNode: undefined, + onMouseOut: undefined, + onMouseOverNode: undefined, + opacity: 1, + renderLabel: true, + size: 200, + stroke: 'yellow', + strokeWidth: 2, + type: 'circle' + }); + }); }); describe('when node to build is the highlightedLink target (or source)', () => { describe('and highlight degree is 0', () => { - + test('should properly build node props ()', () => { + that.config.highlightDegree = 0; + + const props = graphHelper.buildNodeProps(that.node, that.config, undefined, { + source: 'some other id', + target: 'id' + }, undefined, 1); + + expect(props).toEqual({ + className: 'node', + cursor: 'pointer', + cx: 1, + cy: 2, + dx: 11.5, + fill: 'green', + fontSize: 8, + fontWeight: 'normal', + id: 'id', + label: 'id', + onClickNode: undefined, + onMouseOut: undefined, + onMouseOverNode: undefined, + opacity: undefined, + renderLabel: true, + size: 200, + stroke: 'none', + strokeWidth: 1.5, + type: 'circle' + }); + }); }); describe('and highlight degree is bigger then 0', () => { - - }); - }); - describe('when node to build is neither highlightedNode neither highlightedLink source/target', () => { - beforeEach(() => { - // set common node structure so that below we only override small stuff - }); - describe('and config.node.highlightColor is "SAME"', () => { - - }); - describe('and config.node.highlightStrokeColor is "SAME"', () => { - - }); - describe('and config.node.labelProperty some label property (not id)', () => { - - }); - describe('and node has a self (node level) color', () => { - - }); - describe('and node has a self (node level) symbolType', () => { - + test('should properly build node props', () => { + that.config.highlightDegree = 2; + + const props = graphHelper.buildNodeProps(that.node, that.config, undefined, { + source: 'some other id', + target: 'id' + }, undefined, 1); + + expect(props).toEqual({ + className: 'node', + cursor: 'pointer', + cx: 1, + cy: 2, + dx: 11.5, + fill: 'green', + fontSize: 8, + fontWeight: 'normal', + id: 'id', + label: 'id', + onClickNode: undefined, + onMouseOut: undefined, + onMouseOverNode: undefined, + opacity: undefined, + renderLabel: true, + size: 200, + stroke: 'none', + strokeWidth: 1.5, + type: 'circle' + }); + }); }); }); }); From a17939cb95f68e682c8150ff831bc959f215dbff Mon Sep 17 00:00:00 2001 From: danielcaldas Date: Mon, 29 Jan 2018 23:15:53 +0000 Subject: [PATCH 8/8] Enforce valid jsdoc via eslint --- .eslintrc.js | 4 +++- src/components/graph/graph.helper.js | 1 + src/components/graph/index.jsx | 17 +++++++++++++++-- src/components/link/index.jsx | 3 +++ src/components/node/index.jsx | 3 +++ 5 files changed, 25 insertions(+), 3 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 321a593b0..f3378d0cd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -29,6 +29,8 @@ module.exports = { "newline-after-var": ["error", "always"], "no-nested-ternary": "error", "no-useless-constructor": "error", - "semi": "error" + "semi": "error", + "require-jsdoc": "error", + "valid-jsdoc": "error" } }; diff --git a/src/components/graph/graph.helper.js b/src/components/graph/graph.helper.js index a54ec6f9b..05492ca10 100644 --- a/src/components/graph/graph.helper.js +++ b/src/components/graph/graph.helper.js @@ -142,6 +142,7 @@ function _initializeNodes(graphNodes) { * @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} */ function _validateGraphData(data) { if (!data.nodes || !data.nodes.length) { diff --git a/src/components/graph/index.jsx b/src/components/graph/index.jsx index 9f64924e7..2f7921809 100644 --- a/src/components/graph/index.jsx +++ b/src/components/graph/index.jsx @@ -98,6 +98,7 @@ const D3_CONST = { export default class Graph extends React.Component { /** * Sets d3 tick function and configures other d3 stuff such as forces and drag events. + * @returns {undefined} */ _graphForcesConfig() { this.state.simulation.nodes(this.state.d3Nodes).on('tick', this._tick); @@ -119,6 +120,7 @@ export default class Graph extends React.Component { /** * Handles d3 drag 'end' event. + * @returns {undefined} */ _onDragEnd = () => !this.state.config.staticGraph && this.state.config.automaticRearrangeAfterDropNode @@ -126,12 +128,12 @@ export default class Graph extends React.Component { /** * Handles d3 'drag' event. + * {@link https://github.com/d3/d3-drag/blob/master/README.md#drag_subject|more about d3 drag} * @param {Object} ev - if not undefined it will contain event data. * @param {number} index - index of the node that is being dragged. * @param {Array.} nodeList - array of d3 nodes. This list of nodes is provided by d3, each * node contains all information that was previously fed by rd3g. - * - * {@link https://github.com/d3/d3-drag/blob/master/README.md#drag_subject|more about d3 drag} + * @returns {undefined} */ _onDragMove = (ev, index, nodeList) => { const id = nodeList[index].id; @@ -153,6 +155,7 @@ export default class Graph extends React.Component { /** * Handles d3 drag 'start' event. + * @returns {undefined} */ _onDragStart = () => this.pauseSimulation(); @@ -160,6 +163,7 @@ export default class Graph extends React.Component { * Sets nodes and links highlighted value. * @param {string} id - the id of the node to highlight. * @param {boolean} [value=false] - the highlight value to be set (true or false). + * @returns {undefined} */ _setNodeHighlightedValue = (id, value=false) => { this.state.highlightedNode = value ? id : ''; @@ -178,12 +182,14 @@ export default class Graph extends React.Component { /** * The tick function simply calls React set state in order to update component and render nodes * along time as d3 calculates new node positioning. + * @returns {undefined} */ _tick = () => this.setState(this.state || {}); /** * Configures zoom upon graph with default or user provided values.
* {@link https://github.com/d3/d3-zoom#zoom} + * @returns {undefined} */ _zoomConfig = () => d3Select(`#${this.state.id}-${CONST.GRAPH_WRAPPER_ID}`) .call(d3Zoom().scaleExtent([this.state.config.minZoom, this.state.config.maxZoom]) @@ -204,6 +210,7 @@ export default class Graph extends React.Component { /** * Handles mouse over node event. * @param {string} id - id of the node that participates in the event. + * @returns {undefined} */ onMouseOverNode = (id) => { this.props.onMouseOverNode && this.props.onMouseOverNode(id); @@ -214,6 +221,7 @@ export default class Graph extends React.Component { /** * Handles mouse out node event. * @param {string} id - id of the node that participates in the event. + * @returns {undefined} */ onMouseOutNode = (id) => { this.props.onMouseOutNode && this.props.onMouseOutNode(id); @@ -225,6 +233,7 @@ export default class Graph extends React.Component { * Handles mouse over link event. * @param {string} source - id of the source node that participates in the event. * @param {string} target - id of the target node that participates in the event. + * @returns {undefined} */ onMouseOverLink = (source, target) => { this.props.onMouseOverLink && this.props.onMouseOverLink(source, target); @@ -240,6 +249,7 @@ export default class Graph extends React.Component { * Handles mouse out link event. * @param {string} source - id of the source node that participates in the event. * @param {string} target - id of the target node that participates in the event. + * @returns {undefined} */ onMouseOutLink = (source, target) => { this.props.onMouseOutLink && this.props.onMouseOutLink(source, target); @@ -254,6 +264,7 @@ export default class Graph extends React.Component { /** * Calls d3 simulation.stop().
* {@link https://github.com/d3/d3-force#simulation_stop} + * @returns {undefined} */ pauseSimulation = () => !this.state.config.staticGraph && this.state.simulation.stop(); @@ -261,6 +272,7 @@ export default class Graph extends React.Component { * This method resets all nodes fixed positions by deleting the properties fx (fixed x) * and fy (fixed y). Following this, a simulation is triggered in order to force nodes to go back * to their original positions (or at least new positions according to the d3 force parameters). + * @returns {undefined} */ resetNodesPositions = () => { if (!this.state.config.staticGraph) { @@ -282,6 +294,7 @@ export default class Graph extends React.Component { /** * Calls d3 simulation.restart().
* {@link https://github.com/d3/d3-force#simulation_restart} + * @returns {undefined} */ restartSimulation = () => !this.state.config.staticGraph && this.state.simulation.restart(); diff --git a/src/components/link/index.jsx b/src/components/link/index.jsx index c0d7fe235..8a75de1b2 100644 --- a/src/components/link/index.jsx +++ b/src/components/link/index.jsx @@ -33,18 +33,21 @@ import React from 'react'; export default class Link extends React.Component { /** * Handle link click event. + * @returns {undefined} */ handleOnClickLink = () => this.props.onClickLink && this.props.onClickLink(this.props.source, this.props.target); /** * Handle mouse over link event. + * @returns {undefined} */ handleOnMouseOverLink = () => this.props.onMouseOverLink && this.props.onMouseOverLink(this.props.source, this.props.target); /** * Handle mouse out link event. + * @returns {undefined} */ handleOnMouseOutLink = () => this.props.onMouseOutLink && this.props.onMouseOutLink(this.props.source, this.props.target); diff --git a/src/components/node/index.jsx b/src/components/node/index.jsx index fc68b95a4..5a2054034 100644 --- a/src/components/node/index.jsx +++ b/src/components/node/index.jsx @@ -42,16 +42,19 @@ import nodeHelper from './helper'; export default class Node extends React.Component { /** * Handle click on the node. + * @returns {undefined} */ handleOnClickNode = () => this.props.onClickNode && this.props.onClickNode(this.props.id); /** * Handle mouse over node event. + * @returns {undefined} */ handleOnMouseOverNode = () => this.props.onMouseOverNode && this.props.onMouseOverNode(this.props.id); /** * Handle mouse out node event. + * @returns {undefined} */ handleOnMouseOutNode = () => this.props.onMouseOut && this.props.onMouseOut(this.props.id);