diff --git a/.babelrc b/.babelrc
index edf9c8e5e..94bd4e6a0 100644
--- a/.babelrc
+++ b/.babelrc
@@ -2,6 +2,7 @@
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": [
"@babel/plugin-proposal-class-properties",
- "@babel/plugin-proposal-object-rest-spread"
+ "@babel/plugin-proposal-object-rest-spread",
+ "@babel/plugin-proposal-optional-chaining",
]
}
diff --git a/.eslintrc.js b/.eslintrc.js
index 021ae755e..dd1352026 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -17,7 +17,7 @@ module.exports = {
jsx: true,
},
},
- plugins: ["standard", "promise", "react", "jest", "cypress"],
+ plugins: ["standard", "promise", "react", "jest", "cypress", "babel"],
env: {
browser: true,
},
@@ -27,7 +27,7 @@ module.exports = {
camelcase: "error",
"keyword-spacing": "error",
"max-len": ["error", 120, 4, { ignoreComments: true }],
- "max-lines": ["error", { max: 400, skipComments: true }],
+ "max-lines": ["error", { max: 450, skipComments: true }],
"newline-after-var": ["error", "always"],
"no-nested-ternary": "error",
"no-useless-constructor": "error",
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 000000000..01b6482d4
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "javascript.validate.enable": false
+}
diff --git a/README.md b/README.md
index 2a4f09a60..4445270a0 100644
--- a/README.md
+++ b/README.md
@@ -148,7 +148,15 @@ const onNodePositionChange = function(nodeId, x, y) {
Contributions are welcome fell free to submit new ideas/features, just open an issue or send me an email or something. If you are more a _hands on_ person, just submit a pull request. Before jumping into coding, please take at the contribution guidelines [CONTRIBUTING.md](https://github.com/danielcaldas/react-d3-graph/blob/master/CONTRIBUTING.md).
-To run react-d3-graph in development mode you just need to run `npm run dev` and the interactive sandbox will reload with the changes to the library code, that way you can test your changes not only through unit test but also through a real life example. It's that simple.
+To run react-d3-graph in development mode you just need to run `npm run dev` and the interactive sandbox will reload with the changes to the library code, that way you can test your changes not only through unit test but also through a real life example. It's that simple. The development workflow usually should follow the steps:
+
+- Create a branch prefixed with `fix/` for bug fixes, `feature/` for new features, `chore/` or `refactor/` for refactoring or tolling and CI/CD related tasks.
+- Make sure you are up to date running `npm install`.
+- Run `npm run dev`.
+- Do you changes inside the folder `src` and the interactive sandbox consumes your changes in real time
+ with webpack-dev-server.
+- You can run tests locally with `npm run test` (for unit tests) or `npm run functional:local` for e2e tests.
+- After you're done open the Pull Request and describe the changes you've made.
## Alternatives (Not what you where looking for?)
diff --git a/cypress/integration/graph.directed.e2e.js b/cypress/integration/graph.directed.e2e.js
index b8d0ed045..aefc6583a 100644
--- a/cypress/integration/graph.directed.e2e.js
+++ b/cypress/integration/graph.directed.e2e.js
@@ -64,9 +64,15 @@ describe("[rd3g-graph-directed]", function() {
this.link34PO = new LinkPO(3);
});
+ afterEach(function() {
+ this.sandboxPO.exitFullScreenMode();
+ });
+
it("should behave correctly when directed is disabled after collapsed node", function() {
const toggledLine = this.link12PO.getLine();
+ this.sandboxPO.fullScreenMode().click();
+
// Check the leaf node & link is present
this.node2PO.getPath().should("be.visible");
toggledLine.should("be.visible");
@@ -87,10 +93,14 @@ describe("[rd3g-graph-directed]", function() {
this.link14PO.getLine().should("be.visible");
this.link34PO.getLine().should("be.visible");
+ this.sandboxPO.exitFullScreenMode();
+
// Disable "directed"
cy.contains("directed").scrollIntoView();
this.sandboxPO.getFieldInput("directed").click();
+ this.sandboxPO.fullScreenMode().click();
+
// Check if other nodes and links are still visible
this.node1PO.getPath().should("be.visible");
this.node2PO.getPath().should("be.visible");
@@ -104,6 +114,8 @@ describe("[rd3g-graph-directed]", function() {
});
it("should behave correctly when collapsible is disabled after collapsible node", function() {
+ this.sandboxPO.fullScreenMode().click();
+
const toggledLine = this.link12PO.getLine();
// Check the leaf node & link is present
@@ -126,10 +138,14 @@ describe("[rd3g-graph-directed]", function() {
this.link14PO.getLine().should("be.visible");
this.link34PO.getLine().should("be.visible");
+ this.sandboxPO.exitFullScreenMode();
+
// Disable "collapsible"
cy.contains("collapsible").scrollIntoView();
this.sandboxPO.getFieldInput("collapsible").click();
+ this.sandboxPO.fullScreenMode().click();
+
// The previously hidden node should reappear
this.node2PO.getPath().should("be.visible");
toggledLine.should("be.visible");
diff --git a/cypress/integration/graph.e2e.js b/cypress/integration/graph.e2e.js
index 13d32a0a3..1c5bcf171 100644
--- a/cypress/integration/graph.e2e.js
+++ b/cypress/integration/graph.e2e.js
@@ -163,6 +163,12 @@ describe("[rd3g-graph] graph tests", function() {
this.node3PO = new NodePO(3);
this.node4PO = new NodePO(4);
this.link12PO = new LinkPO(0);
+
+ this.sandboxPO.fullScreenMode().click();
+ });
+
+ afterEach(function() {
+ this.sandboxPO.exitFullScreenMode();
});
it("should collapse leaf nodes", function() {
diff --git a/package.json b/package.json
index 466fbe202..fccc06e2e 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
"devDependencies": {
"@babel/core": "7.6.0",
"@babel/plugin-proposal-class-properties": "7.5.5",
+ "@babel/plugin-proposal-optional-chaining": "7.6.0",
"@babel/preset-env": "7.6.0",
"@babel/preset-react": "7.0.0",
"@cypress/webpack-preprocessor": "4.1.0",
@@ -52,6 +53,7 @@
"documentation": "12.1.2",
"eslint": "6.3.0",
"eslint-config-recommended": "4.0.0",
+ "eslint-plugin-babel": "5.3.0",
"eslint-plugin-cypress": "2.6.1",
"eslint-plugin-jest": "22.17.0",
"eslint-plugin-prettier": "3.1.0",
diff --git a/sandbox/Sandbox.jsx b/sandbox/Sandbox.jsx
index c5f443925..b0c7e0310 100644
--- a/sandbox/Sandbox.jsx
+++ b/sandbox/Sandbox.jsx
@@ -7,11 +7,11 @@ import "./styles.css";
import defaultConfig from "../src/components/graph/graph.config";
import { Graph } from "../src";
-import utils from "./utils";
-import reactD3GraphUtils from "../src/utils";
+import { generateFormSchema, loadDataset, setValue } from "./utils";
+import { isDeepEqual, merge } from "../src/utils";
import { JsonTree } from "react-editable-json-tree";
-const sandboxData = utils.loadDataset();
+const sandboxData = loadDataset();
/**
* This is a sample integration of react-d3-graph, in this particular case all the rd3g config properties
@@ -27,7 +27,7 @@ export default class Sandbox extends React.Component {
const { config: configOverride, data, fullscreen } = sandboxData;
const config = Object.assign(defaultConfig, configOverride);
- const schemaProps = utils.generateFormSchema(config, "", {});
+ const schemaProps = generateFormSchema(config, "", {});
const schema = {
type: "object",
@@ -179,7 +179,7 @@ export default class Sandbox extends React.Component {
for (let k of Object.keys(data.formData)) {
// Set value mapping correctly for config object of react-d3-graph
- utils.setValue(config, k, data.formData[k]);
+ setValue(config, k, data.formData[k]);
// Set new values for schema of jsonform
schemaPropsValues[k] = {};
schemaPropsValues[k]["default"] = data.formData[k];
@@ -191,7 +191,7 @@ export default class Sandbox extends React.Component {
refreshGraph = data => {
const { config, schemaPropsValues } = this._buildGraphConfig(data);
- this.state.schema.properties = reactD3GraphUtils.merge(this.state.schema.properties, schemaPropsValues);
+ this.state.schema.properties = merge(this.state.schema.properties, schemaPropsValues);
this.setState({
config,
@@ -215,7 +215,7 @@ export default class Sandbox extends React.Component {
resetGraphConfig = () => {
const generatedConfig = {};
- const schemaProps = utils.generateFormSchema(defaultConfig, "", {});
+ const schemaProps = generateFormSchema(defaultConfig, "", {});
const schema = {
type: "object",
@@ -445,7 +445,7 @@ export default class Sandbox extends React.Component {
class JSONContainer extends React.Component {
shouldComponentUpdate(nextProps) {
- return !this.props.staticData && !reactD3GraphUtils.isDeepEqual(nextProps.data, this.props.data);
+ return !this.props.staticData && !isDeepEqual(nextProps.data, this.props.data);
}
render() {
diff --git a/sandbox/styles.css b/sandbox/styles.css
index 68af40f92..f8f6020e1 100644
--- a/sandbox/styles.css
+++ b/sandbox/styles.css
@@ -57,7 +57,7 @@
.container__form {
grid-column: 5/ 6;
grid-row: 1 / 4;
- min-width: 250px;
+ min-width: 400px;
z-index: 3;
}
diff --git a/sandbox/utils.js b/sandbox/utils.js
index fd54f9760..c1a435156 100644
--- a/sandbox/utils.js
+++ b/sandbox/utils.js
@@ -2,7 +2,7 @@
import queryString from "query-string";
import { LINE_TYPES } from "../src/components/link/link.const";
import DEFAULT_CONFIG from "../src/components/graph/graph.config";
-import utils from "../src/utils";
+import { merge } from "../src/utils";
/**
* This two functions generate the react-jsonschema-form
@@ -57,7 +57,7 @@ function loadDataset() {
try {
const data = require(`./data/${dataset}/${dataset}.data`);
const datasetConfig = require(`./data/${dataset}/${dataset}.config`);
- const config = utils.merge(DEFAULT_CONFIG, datasetConfig);
+ const config = merge(DEFAULT_CONFIG, datasetConfig);
return { data, config, fullscreen };
} catch (error) {
@@ -90,8 +90,4 @@ function setValue(obj, access, value) {
access.length > 1 ? setValue(obj[access.shift()], access, value) : (obj[access[0]] = value);
}
-export default {
- generateFormSchema,
- loadDataset,
- setValue,
-};
+export { generateFormSchema, loadDataset, setValue };
diff --git a/src/components/graph/Graph.jsx b/src/components/graph/Graph.jsx
index e115ad8ce..b15b6fedf 100644
--- a/src/components/graph/Graph.jsx
+++ b/src/components/graph/Graph.jsx
@@ -9,10 +9,16 @@ import CONST from "./graph.const";
import DEFAULT_CONFIG from "./graph.config";
import ERRORS from "../../err";
-import * as collapseHelper from "./collapse.helper";
-import * as graphHelper from "./graph.helper";
-import * as graphRenderer from "./graph.renderer";
-import utils from "../../utils";
+import { getTargetLeafConnections, toggleLinksMatrixConnections, toggleLinksConnections } from "./collapse.helper";
+import {
+ updateNodeHighlightedValue,
+ checkForGraphConfigChanges,
+ checkForGraphElementsChanges,
+ getCenterAndZoomTransformation,
+ initializeGraphState,
+} from "./graph.helper";
+import { renderGraph } from "./graph.renderer";
+import { merge, throwErr } from "../../utils";
/**
* Graph component is the main component for react-d3-graph components, its interface allows its user
@@ -186,8 +192,11 @@ export default class Graph extends React.Component {
* @returns {undefined}
*/
_onDragEnd = () => {
- this.state.draggedNode &&
- this.onNodePositionChange(this.state.draggedNode.id, this.state.draggedNode.fx, this.state.draggedNode.fy);
+ if (this.state.draggedNode) {
+ this.onNodePositionChange(this.state.draggedNode);
+ this._tick({ draggedNode: null });
+ }
+
!this.state.config.staticGraph &&
this.state.config.automaticRearrangeAfterDropNode &&
this.state.simulation.alphaTarget(this.state.config.d3.alphaTarget).restart();
@@ -209,6 +218,9 @@ export default class Graph extends React.Component {
// this is where d3 and react bind
let draggedNode = this.state.nodes[id];
+ draggedNode.oldX = draggedNode.x;
+ draggedNode.oldY = draggedNode.y;
+
draggedNode.x += d3Event.dx;
draggedNode.y += d3Event.dy;
@@ -238,9 +250,7 @@ export default class Graph extends React.Component {
* @returns {undefined}
*/
_setNodeHighlightedValue = (id, value = false) =>
- this._tick(
- graphHelper.updateNodeHighlightedValue(this.state.nodes, this.state.links, this.state.config, id, value)
- );
+ this._tick(updateNodeHighlightedValue(this.state.nodes, this.state.links, this.state.config, id, value));
/**
* The tick function simply calls React set state in order to update component and render nodes
@@ -295,7 +305,7 @@ export default class Graph extends React.Component {
// toUpperCase() is added as a precaution, as the documentation says tagName should always
// return in UPPERCASE, but chrome returns lowercase
const tagName = e.target && e.target.tagName;
- const name = e.target && e.target.attributes && e.target.attributes.name && e.target.attributes.name.value;
+ const name = e?.target?.attributes?.name?.value;
const svgContainerName = `svg-container-${this.state.id}`;
if (tagName.toUpperCase() === "SVG" && name === svgContainerName) {
@@ -310,18 +320,10 @@ export default class Graph extends React.Component {
*/
onClickNode = clickedNodeId => {
if (this.state.config.collapsible) {
- const leafConnections = collapseHelper.getTargetLeafConnections(
- clickedNodeId,
- this.state.links,
- this.state.config
- );
- const links = collapseHelper.toggleLinksMatrixConnections(
- this.state.links,
- leafConnections,
- this.state.config
- );
- const d3Links = collapseHelper.toggleLinksConnections(this.state.d3Links, links);
- const firstLeaf = leafConnections && leafConnections.length && leafConnections[0];
+ const leafConnections = getTargetLeafConnections(clickedNodeId, this.state.links, this.state.config);
+ const links = toggleLinksMatrixConnections(this.state.links, leafConnections, this.state.config);
+ const d3Links = toggleLinksConnections(this.state.d3Links, links);
+ const firstLeaf = leafConnections?.["0"];
let isExpanding = false;
@@ -413,13 +415,22 @@ export default class Graph extends React.Component {
/**
* Handles node position change.
- * @param {string} nodeId - id of the node whose position changed.
- * @param {number} x - x coordinate of the node whose position changed.
- * @param {number} y - y coordinate of the node whose position changed.
+ * @param {Object} node - an object holding information about the dragged node.
* @returns {undefined}
*/
- onNodePositionChange = (nodeId, x, y) =>
- this.props.onNodePositionChange && this.props.onNodePositionChange(nodeId, x, y);
+ onNodePositionChange = node => {
+ if (!this.props.onNodePositionChange) {
+ return;
+ }
+
+ const { id, oldX, oldY, x, y } = node;
+ const deltaX = x - oldX;
+ const deltaY = y - oldY;
+
+ if (deltaX !== 0 || deltaY !== 0) {
+ this.props.onNodePositionChange(id, x, y);
+ }
+ };
/**
* Calls d3 simulation.stop().
@@ -462,11 +473,12 @@ export default class Graph extends React.Component {
super(props);
if (!this.props.id) {
- utils.throwErr(this.constructor.name, ERRORS.GRAPH_NO_ID_PROP);
+ throwErr(this.constructor.name, ERRORS.GRAPH_NO_ID_PROP);
}
this.focusAnimationTimeout = null;
- this.state = graphHelper.initializeGraphState(this.props, this.state);
+ this.nodeClickTimer = null;
+ this.state = initializeGraphState(this.props, this.state);
}
/**
@@ -481,14 +493,11 @@ export default class Graph extends React.Component {
*/
// eslint-disable-next-line
UNSAFE_componentWillReceiveProps(nextProps) {
- const { graphElementsUpdated, newGraphElements } = graphHelper.checkForGraphElementsChanges(
- nextProps,
- this.state
- );
- const state = graphElementsUpdated ? graphHelper.initializeGraphState(nextProps, this.state) : this.state;
+ const { graphElementsUpdated, newGraphElements } = checkForGraphElementsChanges(nextProps, this.state);
+ const state = graphElementsUpdated ? initializeGraphState(nextProps, this.state) : this.state;
const newConfig = nextProps.config || {};
- const { configUpdated, d3ConfigUpdated } = graphHelper.checkForGraphConfigChanges(nextProps, this.state);
- const config = configUpdated ? utils.merge(DEFAULT_CONFIG, newConfig) : this.state.config;
+ const { configUpdated, d3ConfigUpdated } = checkForGraphConfigChanges(nextProps, this.state);
+ const config = configUpdated ? merge(DEFAULT_CONFIG, newConfig) : this.state.config;
// in order to properly update graph data we need to pause eventual d3 ongoing animations
newGraphElements && this.pauseSimulation();
@@ -496,7 +505,7 @@ export default class Graph extends React.Component {
const transform = newConfig.panAndZoom !== this.state.config.panAndZoom ? 1 : this.state.transform;
const focusedNodeId = nextProps.data.focusedNodeId;
const d3FocusedNode = this.state.d3Nodes.find(node => `${node.id}` === `${focusedNodeId}`);
- const focusTransformation = graphHelper.getCenterAndZoomTransformation(d3FocusedNode, this.state.config);
+ const focusTransformation = getCenterAndZoomTransformation(d3FocusedNode, this.state.config);
const enableFocusAnimation = this.props.data.focusedNodeId !== nextProps.data.focusedNodeId;
this.setState({
@@ -549,11 +558,20 @@ export default class Graph extends React.Component {
componentWillUnmount() {
this.pauseSimulation();
- this.nodeClickTimer && clearTimeout(this.nodeClickTimer);
+
+ if (this.nodeClickTimer) {
+ clearTimeout(this.nodeClickTimer);
+ this.nodeClickTimer = null;
+ }
+
+ if (this.focusAnimationTimeout) {
+ clearTimeout(this.focusAnimationTimeout);
+ this.focusAnimationTimeout = null;
+ }
}
render() {
- const { nodes, links, defs } = graphRenderer.renderGraph(
+ const { nodes, links, defs } = renderGraph(
this.state.nodes,
{
onClickNode: this.onClickNode,
diff --git a/src/components/graph/collapse.helper.js b/src/components/graph/collapse.helper.js
index 24a5e734b..3496b1e27 100644
--- a/src/components/graph/collapse.helper.js
+++ b/src/components/graph/collapse.helper.js
@@ -2,22 +2,10 @@
* @module Graph/collapse-helper
* @description
* Offers a series of methods that allow graph to perform the necessary operations to
- * create the collapsible behavior.
- *
- * Developer notes - collapsing nodes and maintaining state on links matrix.
- *
- * User interaction flow (for a collapsible graph)
- * 1. User clicks node
- * 2. All leaf connections of that node are not rendered anymore
- * 3. User clicks on same node
- * 4. All leaf connections of that node are rendered
- *
- * Internal react-d3-graph flow
- * 1. User clicks node
- * 2. Compute leaf connections for clicked node (rootNode, root as in 'root' of the event)
- * 3. Update connections matrix (based on 2.)
- * 4. Update d3Links array with toggled connections (based on 2.)
+ * create the collapsible behavior. These functions will most likely operate on
+ * the links matrix.
*/
+import { getId } from "./graph.helper";
/**
* For directed graphs.
@@ -53,8 +41,9 @@ function _isLeafNotDirected(inDegree, outDegree) {
*/
function _isLeaf(nodeId, linksMatrix, directed) {
const { inDegree, outDegree } = computeNodeDegree(nodeId, linksMatrix);
+ const fn = directed ? _isLeafDirected : _isLeafNotDirected;
- return directed ? _isLeafDirected(inDegree, outDegree) : _isLeafNotDirected(inDegree, outDegree);
+ return fn(inDegree, outDegree);
}
/**
@@ -78,17 +67,11 @@ function computeNodeDegree(nodeId, linksMatrix = {}) {
return currentNodeConnections.reduce((_acc, target) => {
if (nodeId === source) {
- return {
- ..._acc,
- outDegree: _acc.outDegree + linksMatrix[nodeId][target],
- };
+ _acc.outDegree += linksMatrix[nodeId][target];
}
if (nodeId === target) {
- return {
- ..._acc,
- inDegree: _acc.inDegree + linksMatrix[source][nodeId],
- };
+ _acc.inDegree += linksMatrix[source][nodeId];
}
return _acc;
@@ -133,7 +116,7 @@ function getTargetLeafConnections(rootNodeId, linksMatrix = {}, { directed }) {
* NOTE: this function is meant to be used under the `collapsible` toggle, meaning
* that the `isNodeVisible` actually is checking visibility on collapsible graphs.
* If you think that this code is confusing and could potentially collide (🤞) with #_isLeaf
- * always remember that *A leaf can, through time, be both a visible or an invisible node!*.
+ * always remember that *A leaf can, throughout time, both a visible or an invisible node!*.
*
* @param {string} nodeId - The id of the node to get the cardinality of
* @param {Object.} nodes - an object containing all nodes mapped by their id.
@@ -142,10 +125,13 @@ function getTargetLeafConnections(rootNodeId, linksMatrix = {}, { directed }) {
* @memberof Graph/collapse-helper
*/
function isNodeVisible(nodeId, nodes, linksMatrix) {
+ if (nodes[nodeId]._orphan) {
+ return true;
+ }
+
const { inDegree, outDegree } = computeNodeDegree(nodeId, linksMatrix);
- const orphan = !!nodes[nodeId]._orphan;
- return inDegree > 0 || outDegree > 0 || orphan;
+ return inDegree > 0 || outDegree > 0;
}
/**
@@ -158,12 +144,13 @@ function isNodeVisible(nodeId, nodes, linksMatrix) {
function toggleLinksConnections(d3Links, connectionMatrix) {
return d3Links.map(d3Link => {
const { source, target } = d3Link;
- const sourceId = source.id || source;
- const targetId = target.id || target;
+ const sourceId = getId(source);
+ const targetId = getId(target);
// connectionMatrix[sourceId][targetId] can be 0 or non existent
const connection = connectionMatrix && connectionMatrix[sourceId] && connectionMatrix[sourceId][targetId];
+ const isHidden = !connection;
- return connection ? { ...d3Link, isHidden: false } : { ...d3Link, isHidden: true };
+ return { ...d3Link, isHidden };
});
}
diff --git a/src/components/graph/graph.builder.js b/src/components/graph/graph.builder.js
index 44ece2294..7648ae9e4 100644
--- a/src/components/graph/graph.builder.js
+++ b/src/components/graph/graph.builder.js
@@ -53,10 +53,10 @@ function _getNodeOpacity(node, highlightedNode, highlightedLink, config) {
*/
function buildLinkProps(link, nodes, links, config, linkCallbacks, highlightedNode, highlightedLink, transform) {
const { source, target } = link;
- 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;
+ const x1 = nodes?.[source]?.x || 0;
+ const y1 = nodes?.[source]?.y || 0;
+ const x2 = nodes?.[target]?.x || 0;
+ const y2 = nodes?.[target]?.y || 0;
const d = buildLinkPathDefinition({ source: { x: x1, y: y1 }, target: { x: x2, y: y2 } }, config.link.type);
@@ -74,11 +74,11 @@ function buildLinkProps(link, nodes, links, config, linkCallbacks, highlightedNo
break;
}
- const reasonNode = mainNodeParticipates && nodes[source].highlighted && nodes[target].highlighted;
- const reasonLink =
+ const guiltyNode = mainNodeParticipates && nodes[source].highlighted && nodes[target].highlighted;
+ const guiltyLink =
source === (highlightedLink && highlightedLink.source) &&
target === (highlightedLink && highlightedLink.target);
- const highlight = reasonNode || reasonLink;
+ const highlight = guiltyNode || guiltyLink;
let opacity = link.opacity || config.link.opacity;
@@ -122,23 +122,23 @@ function buildLinkProps(link, nodes, links, config, linkCallbacks, highlightedNo
}
return {
- markerId,
+ className: CONST.LINK_CLASS_NAME,
d,
- source,
- target,
- strokeWidth,
- stroke,
- label,
- mouseCursor: config.link.mouseCursor,
fontColor,
fontSize: fontSize * t,
fontWeight,
- className: CONST.LINK_CLASS_NAME,
+ label,
+ markerId,
+ mouseCursor: config.link.mouseCursor,
opacity,
+ source,
+ stroke,
+ strokeWidth,
+ target,
onClickLink: linkCallbacks.onClickLink,
- onRightClickLink: linkCallbacks.onRightClickLink,
- onMouseOverLink: linkCallbacks.onMouseOverLink,
onMouseOutLink: linkCallbacks.onMouseOutLink,
+ onMouseOverLink: linkCallbacks.onMouseOverLink,
+ onRightClickLink: linkCallbacks.onRightClickLink,
};
}
@@ -195,20 +195,17 @@ function buildNodeProps(node, config, nodeCallbacks = {}, highlightedNode, highl
...node,
className: CONST.NODE_CLASS_NAME,
cursor: config.node.mouseCursor,
- cx: (node && node.x) || "0",
- cy: (node && node.y) || "0",
+ cx: node?.x || "0",
+ cy: node?.y || "0",
+ dx,
fill,
fontColor,
fontSize: fontSize * t,
- dx,
fontWeight: highlight ? config.node.highlightFontWeight : config.node.fontWeight,
id: node.id,
label,
- onClickNode: nodeCallbacks.onClickNode,
- onRightClickNode: nodeCallbacks.onRightClickNode,
- onMouseOverNode: nodeCallbacks.onMouseOverNode,
- onMouseOut: nodeCallbacks.onMouseOut,
opacity,
+ overrideGlobalViewGenerator: !node.viewGenerator && node.svg,
renderLabel: config.node.renderLabel,
size: nodeSize * t,
stroke,
@@ -216,7 +213,10 @@ function buildNodeProps(node, config, nodeCallbacks = {}, highlightedNode, highl
svg,
type: node.symbolType || config.node.symbolType,
viewGenerator: node.viewGenerator || config.node.viewGenerator,
- overrideGlobalViewGenerator: !node.viewGenerator && node.svg,
+ onClickNode: nodeCallbacks.onClickNode,
+ onMouseOut: nodeCallbacks.onMouseOut,
+ onMouseOverNode: nodeCallbacks.onMouseOverNode,
+ onRightClickNode: nodeCallbacks.onRightClickNode,
};
}
diff --git a/src/components/graph/graph.helper.js b/src/components/graph/graph.helper.js
index bc9927851..277ff08d7 100644
--- a/src/components/graph/graph.helper.js
+++ b/src/components/graph/graph.helper.js
@@ -30,7 +30,7 @@ import CONST from "./graph.const";
import DEFAULT_CONFIG from "./graph.config";
import ERRORS from "../../err";
-import utils from "../../utils";
+import { isDeepEqual, isEmptyObject, merge, pick, antiPick, throwErr } from "../../utils";
import { computeNodeDegree } from "./collapse.helper";
const NODE_PROPS_WHITELIST = ["id", "highlighted", "x", "y", "index", "vy", "vx"];
@@ -70,8 +70,8 @@ function _createForceSimulation(width, height, gravity) {
*/
function _initializeLinks(graphLinks, config) {
return graphLinks.reduce((links, l) => {
- const source = l.source.id !== undefined && l.source.id !== null ? l.source.id : l.source;
- const target = l.target.id !== undefined && l.target.id !== null ? l.target.id : l.target;
+ const source = getId(l.source);
+ const target = getId(l.target);
if (!links[source]) {
links[source] = {};
@@ -140,8 +140,8 @@ function _initializeNodes(graphNodes) {
function _mergeDataLinkWithD3Link(link, index, d3Links = [], config, state = {}) {
// find the matching link if it exists
const tmp = d3Links.find(l => l.source.id === link.source && l.target.id === link.target);
- const d3Link = tmp && utils.pick(tmp, LINK_PROPS_WHITELIST);
- const customProps = utils.antiPick(link, ["source", "target"]);
+ const d3Link = tmp && pick(tmp, LINK_PROPS_WHITELIST);
+ const customProps = antiPick(link, ["source", "target"]);
if (d3Link) {
const toggledDirected =
@@ -213,7 +213,7 @@ function _tagOrphanNodes(nodes, linksMatrix) {
*/
function _validateGraphData(data) {
if (!data.nodes || !data.nodes.length) {
- utils.throwErr("Graph", ERRORS.INSUFFICIENT_DATA);
+ throwErr("Graph", ERRORS.INSUFFICIENT_DATA);
}
const n = data.links.length;
@@ -222,15 +222,15 @@ function _validateGraphData(data) {
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`);
+ 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`);
+ throwErr("Graph", `${ERRORS.INVALID_LINKS} - "${l.target}" is not a valid target node id`);
}
if (l && l.value !== undefined && typeof l.value !== "number") {
- utils.throwErr(
+ throwErr(
"Graph",
`${ERRORS.INVALID_LINK_VALUE} - found in link with source "${l.source}" and target "${l.target}"`
);
@@ -248,7 +248,7 @@ const NODE_PROPERTIES_DISCARD_TO_COMPARE = ["x", "y", "vx", "vy", "index"];
* @memberof Graph/helper
*/
function _pickId(o) {
- return utils.pick(o, ["id"]);
+ return pick(o, ["id"]);
}
/**
@@ -258,7 +258,7 @@ function _pickId(o) {
* @memberof Graph/helper
*/
function _pickSourceAndTarget(o) {
- return utils.pick(o, ["source", "target"]);
+ return pick(o, ["source", "target"]);
}
/**
@@ -275,22 +275,19 @@ function _pickSourceAndTarget(o) {
* @memberof Graph/helper
*/
function checkForGraphElementsChanges(nextProps, currentState) {
- const nextNodes = nextProps.data.nodes.map(n => utils.antiPick(n, NODE_PROPERTIES_DISCARD_TO_COMPARE));
+ const nextNodes = nextProps.data.nodes.map(n => antiPick(n, NODE_PROPERTIES_DISCARD_TO_COMPARE));
const nextLinks = nextProps.data.links;
- const stateD3Nodes = currentState.d3Nodes.map(n => utils.antiPick(n, NODE_PROPERTIES_DISCARD_TO_COMPARE));
+ const stateD3Nodes = currentState.d3Nodes.map(n => antiPick(n, NODE_PROPERTIES_DISCARD_TO_COMPARE));
const stateD3Links = currentState.d3Links.map(l => ({
- // 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,
+ source: getId(l.source),
+ target: getId(l.target),
}));
- const graphElementsUpdated = !(
- utils.isDeepEqual(nextNodes, stateD3Nodes) && utils.isDeepEqual(nextLinks, stateD3Links)
- );
+ const graphElementsUpdated = !(isDeepEqual(nextNodes, stateD3Nodes) && isDeepEqual(nextLinks, stateD3Links));
const newGraphElements =
nextNodes.length !== stateD3Nodes.length ||
nextLinks.length !== stateD3Links.length ||
- !utils.isDeepEqual(nextNodes.map(_pickId), stateD3Nodes.map(_pickId)) ||
- !utils.isDeepEqual(nextLinks.map(_pickSourceAndTarget), stateD3Links.map(_pickSourceAndTarget));
+ !isDeepEqual(nextNodes.map(_pickId), stateD3Nodes.map(_pickId)) ||
+ !isDeepEqual(nextLinks.map(_pickSourceAndTarget), stateD3Links.map(_pickSourceAndTarget));
return { graphElementsUpdated, newGraphElements };
}
@@ -306,9 +303,8 @@ function checkForGraphElementsChanges(nextProps, currentState) {
*/
function checkForGraphConfigChanges(nextProps, currentState) {
const newConfig = nextProps.config || {};
- const configUpdated =
- newConfig && !utils.isEmptyObject(newConfig) && !utils.isDeepEqual(newConfig, currentState.config);
- const d3ConfigUpdated = newConfig && newConfig.d3 && !utils.isDeepEqual(newConfig.d3, currentState.config.d3);
+ const configUpdated = newConfig && !isEmptyObject(newConfig) && !isDeepEqual(newConfig, currentState.config);
+ const d3ConfigUpdated = newConfig && newConfig.d3 && !isDeepEqual(newConfig.d3, currentState.config.d3);
return { configUpdated, d3ConfigUpdated };
}
@@ -318,7 +314,7 @@ function checkForGraphConfigChanges(nextProps, currentState) {
* 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.
+ * @returns {string|undefined} transform rule to apply.
* @memberof Graph/helper
*/
function getCenterAndZoomTransformation(d3Node, config) {
@@ -335,6 +331,24 @@ function getCenterAndZoomTransformation(d3Node, config) {
`;
}
+/**
+ * This function extracts an id from a link.
+ * **Why this function?**
+ * According to [d3-force](https://github.com/d3/d3-force#link_links)
+ * d3 links might be initialized with "source" and "target"
+ * properties as numbers or strings, but after initialization they
+ * are converted to an object. This small utility functions ensures
+ * that weather in initialization or further into the lifetime of the graph
+ * we always get the id.
+ * @param {Object|string|number} sot source or target
+ * of the link to extract id.
+ * we want to extract an id.
+ * @returns {string|number} the id of the link.
+ */
+function getId(sot) {
+ return sot.id !== undefined && sot.id !== null ? sot.id : sot;
+}
+
/**
* Encapsulates common procedures to initialize graph.
* @param {Object} props - Graph component props, object that holds data, id and config.
@@ -353,7 +367,7 @@ function initializeGraphState({ data, id, config }, state) {
if (state && state.nodes) {
graph = {
nodes: data.nodes.map(n =>
- state.nodes[n.id] ? { ...n, ...utils.pick(state.nodes[n.id], NODE_PROPS_WHITELIST) } : { ...n }
+ state.nodes[n.id] ? { ...n, ...pick(state.nodes[n.id], NODE_PROPS_WHITELIST) } : { ...n }
),
links: data.links.map((l, index) =>
_mergeDataLinkWithD3Link(l, index, state && state.d3Links, config, state)
@@ -366,7 +380,7 @@ function initializeGraphState({ data, id, config }, state) {
};
}
- let newConfig = { ...utils.merge(DEFAULT_CONFIG, config || {}) },
+ let newConfig = { ...merge(DEFAULT_CONFIG, config || {}) },
links = _initializeLinks(graph.links, newConfig), // matrix of graph connections
nodes = _tagOrphanNodes(_initializeNodes(graph.nodes), links);
const { nodes: d3Nodes, links: d3Links } = graph;
@@ -418,7 +432,9 @@ function updateNodeHighlightedValue(nodes, links, config, id, value = false) {
updatedNodes = Object.keys(links[id]).reduce((acc, linkId) => {
const updatedNode = { ...updatedNodes[linkId], highlighted: value };
- return { ...acc, [linkId]: updatedNode };
+ acc[linkId] = updatedNode;
+
+ return acc;
}, updatedNodes);
}
@@ -432,6 +448,7 @@ export {
checkForGraphConfigChanges,
checkForGraphElementsChanges,
getCenterAndZoomTransformation,
+ getId,
initializeGraphState,
updateNodeHighlightedValue,
};
diff --git a/src/components/graph/graph.renderer.jsx b/src/components/graph/graph.renderer.jsx
index 632976585..d70c49b60 100644
--- a/src/components/graph/graph.renderer.jsx
+++ b/src/components/graph/graph.renderer.jsx
@@ -12,6 +12,7 @@ import Link from "../link/Link";
import Node from "../node/Node";
import Marker from "../marker/Marker";
import { buildLinkProps, buildNodeProps } from "./graph.builder";
+import { getId } from "../graph/graph.helper";
import { isNodeVisible } from "./collapse.helper";
/**
@@ -36,9 +37,8 @@ function _renderLinks(nodes, links, linksMatrix, config, linkCallbacks, highligh
return outLinks.map(link => {
const { source, target } = link;
- // FIXME: solve this source data inconsistency later
- const sourceId = source.id !== undefined && source.id !== null ? source.id : source;
- const targetId = target.id !== undefined && target.id !== null ? target.id : target;
+ const sourceId = getId(source);
+ const targetId = getId(target);
const key = `${sourceId}${CONST.COORDS_SEPARATOR}${targetId}`;
const props = buildLinkProps(
{ ...link, source: `${sourceId}`, target: `${targetId}` },
diff --git a/src/components/marker/marker.helper.js b/src/components/marker/marker.helper.js
index 86c66ab68..39b9359e1 100644
--- a/src/components/marker/marker.helper.js
+++ b/src/components/marker/marker.helper.js
@@ -7,7 +7,6 @@ import { MARKERS, SIZES, HIGHLIGHTED } from "./marker.const";
/**
* This function is a key template builder to access MARKERS structure.
- * WARN: function tightly coupled to the MARKERS object in marker.const.
* @param {string} size - string that indicates size of marker.
* @param {string} highlighted - string that indicates highlight state of marker.
* @returns {string} the key of the marker.
diff --git a/src/utils.js b/src/utils.js
index ca30f1d75..1d6d2f7c7 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -94,10 +94,8 @@ function isEmptyObject(o) {
* @memberof utils
*/
function deepClone(o, _clone = {}, _depth = 0) {
- // TODO: Handle invalid input o is null, undefined, empty object
const oKeys = Object.keys(o);
- // TODO: handle arrays
for (let k of oKeys) {
const nested = _isPropertyNestedObject(o, k);
@@ -187,12 +185,4 @@ function throwErr(component, msg) {
throw Error(error);
}
-export default {
- isDeepEqual,
- isEmptyObject,
- deepClone,
- merge,
- pick,
- antiPick,
- throwErr,
-};
+export { isDeepEqual, isEmptyObject, deepClone, merge, pick, antiPick, throwErr };
diff --git a/test/graph/graph.builder.spec.js b/test/graph/graph.builder.spec.js
index 26857821f..35a15f9e9 100644
--- a/test/graph/graph.builder.spec.js
+++ b/test/graph/graph.builder.spec.js
@@ -2,7 +2,7 @@ import * as graphHelper from "../../src/components/graph/graph.builder";
import config from "../../src/components/graph/graph.config";
-import utils from "../../src/utils";
+import * as utils from "../../src/utils";
import * as linkHelper from "../../src/components/link/link.helper";
describe("Graph Helper", () => {
diff --git a/test/graph/graph.helper.spec.js b/test/graph/graph.helper.spec.js
index 9e8c0aad2..d95fef9bf 100644
--- a/test/graph/graph.helper.spec.js
+++ b/test/graph/graph.helper.spec.js
@@ -1,6 +1,6 @@
import * as graphHelper from "../../src/components/graph/graph.helper";
-import utils from "../../src/utils";
+import * as utils from "../../src/utils";
jest.mock("d3-force");
import {
diff --git a/test/utils.spec.js b/test/utils.spec.js
index 6e5b26f9b..5bd7d8d3b 100644
--- a/test/utils.spec.js
+++ b/test/utils.spec.js
@@ -1,4 +1,4 @@
-import utils from "../src/utils";
+import * as utils from "../src/utils";
describe("Utils", () => {
describe("#merge", () => {