Skip to content

Commit

Permalink
Fix/tests coverage (#44)
Browse files Browse the repository at this point in the history
* Add one more test case to utils/isDeepEqual

* Improve utils/merge and add new test case

* Add test to utils/throwErr

* Change tests structure. Add components folders

* Lowercase naming in test files

* Update jest

* Remove plus sign from downloads badge

* Improve createForceSimulation docs

* Add spec for graph helper createForceSimulation

* Organize code in graph helper. Fix lint helper test file

* Improve graph validation err messages

* Graph helper tests for when no valid graph data is provided

* Add tests for initializeGraphState

* initializeGraphState tests

* Improve util test descriptions

* Update graph.helper tests

* Move snapshot tests into separate test folder

* Move _graphForcesConfig private func to top of component
  • Loading branch information
danielcaldas committed Dec 2, 2017
1 parent 9e1a653 commit 72efff6
Show file tree
Hide file tree
Showing 19 changed files with 803 additions and 179 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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-v0.4.0-blue.svg)](https://www.npmjs.com/package/react-d3-graph) [![npm stats](https://img.shields.io/badge/downloads-1k+-brightgreen.svg)](https://npm-stat.com/charts.html?package=react-d3-graph&from=2017-04-25&to=2017-11-24) [![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-v0.4.0-blue.svg)](https://www.npmjs.com/package/react-d3-graph) [![npm stats](https://img.shields.io/badge/downloads-1k-brightgreen.svg)](https://npm-stat.com/charts.html?package=react-d3-graph&from=2017-04-25&to=2017-11-24) [![probot enabled](https://img.shields.io/badge/probot:stale-enabled-yellow.svg)](https://probot.github.io/)
[:book:](https://danielcaldas.github.io/react-d3-graph/docs/index.html)

### *Interactive and configurable graphs with react and d3 effortlessly*
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"lint:test": "node_modules/eslint/bin/eslint.js --config=.eslintrc.test.config.js \"test/**/*.test.js\"",
"test": "jest --verbose --coverage",
"test:clean": "jest --no-cache --updateSnapshot --verbose --coverage",
"test:watch": "jest --verbose --watchAll"
"test:watch": "jest --verbose --coverage --watchAll"
},
"dependencies": {
"d3": "4.10.2",
Expand All @@ -45,7 +45,7 @@
"eslint-plugin-promise": "3.5.0",
"eslint-plugin-standard": "2.1.1",
"html-webpack-plugin": "2.30.1",
"jest": "21.1.0",
"jest": "21.3.0-beta.8",
"npm-run-all": "4.1.1",
"react-addons-test-utils": "15.6.0",
"react-dom": "15.6.1",
Expand Down
241 changes: 120 additions & 121 deletions src/components/graph/helper.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,34 +154,6 @@ function _buildNodeLinks(nodeId, nodes, links, config, linkCallbacks, highlighte
return linksComponents;
}

/**
* 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;
}

/**
* Build some Node properties based on given parameters.
* @param {Object} node - the node object for whom we will generate properties.
Expand Down Expand Up @@ -239,6 +211,119 @@ function _buildNodeProps(node, config, nodeCallbacks, highlightedNode, highlight
};
}

/**
* 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.<string, Object>} an object containing a matrix of connections of the graph, for each nodeId,
* there is an object that maps adjacent nodes ids (string) and their values (number).
* @memberof Graph/helper
*/
function _initializeLinks(graphLinks) {
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.
Expand Down Expand Up @@ -305,8 +390,9 @@ function buildGraph(nodes, nodeCallbacks, links, linkCallbacks, config, highligh

/**
* Create d3 forceSimulation to be applied on the graph.<br/>
* <a href="https://github.com/d3/d3-force#forceSimulation" target="_blank">https://github.com/d3/d3-force#forceSimulation</a><br/>
* <a href="https://github.com/d3/d3-force#simulation_force" target="_blank">https://github.com/d3/d3-force#simulation_force</a><br/>
* {@link https://github.com/d3/d3-force#forceSimulation|d3-force#forceSimulation}<br/>
* {@link https://github.com/d3/d3-force#simulation_force|d3-force#simulation_force}<br/>
* Wtf is a force? {@link https://github.com/d3/d3-force#forces| here}
* @param {number} width - the width of the container area of the graph.
* @param {number} height - the height of the container area of the graph.
* @returns {Object} returns the simulation instance to be consumed.
Expand Down Expand Up @@ -335,7 +421,7 @@ function createForceSimulation(width, height) {
function initializeGraphState({data, id, config}, state) {
let graph;

validateGraphData(data);
_validateGraphData(data);

if (state && state.nodes && state.links) {
// absorb existent positioning
Expand All @@ -353,8 +439,8 @@ function initializeGraphState({data, id, config}, state) {
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
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);
Expand All @@ -374,95 +460,8 @@ function initializeGraphState({data, id, config}, state) {
};
}

/**
* 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.<string, Object>} an object containing a matrix of connections of the graph, for each nodeId,
* there is an object that maps adjacent nodes ids (string) and their values (number).
* @memberof Graph/helper
*/
function initializeLinks(graphLinks) {
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 node id`);
}
if (!data.nodes.find(n => n.id === l.target)) {
utils.throwErr('Graph', `${ERRORS.INVALID_LINKS} - ${l.target} is not a valid node id`);
}
}
}

export default {
buildGraph,
createForceSimulation,
initializeGraphState,
initializeLinks,
initializeNodes
initializeGraphState
};
42 changes: 21 additions & 21 deletions src/components/graph/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,27 @@ const D3_CONST = {
* onMouseOutLink={onMouseOutLink}/>
*/
export default class Graph extends React.Component {
/**
* Sets d3 tick function and configures other d3 stuff such as forces and drag events.
*/
_graphForcesConfig() {
this.state.simulation.nodes(this.state.d3Nodes).on('tick', this._tick);

const forceLink = d3ForceLink(this.state.d3Links)
.id(l => l.id)
.distance(D3_CONST.LINK_IDEAL_DISTANCE)
.strength(D3_CONST.FORCE_LINK_STRENGTH);

this.state.simulation.force(CONST.LINK_CLASS_NAME, forceLink);

const customNodeDrag = d3Drag()
.on('start', this._onDragStart)
.on('drag', this._onDragMove)
.on('end', this._onDragEnd);

d3Select(`#${this.state.id}-${CONST.GRAPH_WRAPPER_ID}`).selectAll('.node').call(customNodeDrag);
}

/**
* Handles d3 drag 'end' event.
*/
Expand Down Expand Up @@ -263,27 +284,6 @@ export default class Graph extends React.Component {
*/
restartSimulation = () => !this.state.config.staticGraph && this.state.simulation.restart();

/**
* Sets d3 tick function and configures other d3 stuff such as forces and drag events.
*/
_graphForcesConfig() {
this.state.simulation.nodes(this.state.d3Nodes).on('tick', this._tick);

const forceLink = d3ForceLink(this.state.d3Links)
.id(l => l.id)
.distance(D3_CONST.LINK_IDEAL_DISTANCE)
.strength(D3_CONST.FORCE_LINK_STRENGTH);

this.state.simulation.force(CONST.LINK_CLASS_NAME, forceLink);

const customNodeDrag = d3Drag()
.on('start', this._onDragStart)
.on('drag', this._onDragMove)
.on('end', this._onDragEnd);

d3Select(`#${this.state.id}-${CONST.GRAPH_WRAPPER_ID}`).selectAll('.node').call(customNodeDrag);
}

constructor(props) {
super(props);

Expand Down
10 changes: 4 additions & 6 deletions src/err.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
/*eslint max-len: ["error", 200]*/
export default {
GRAPH_NO_ID_PROP: "id prop not defined! id property is mandatory and it should be unique.",
STATIC_GRAPH_DATA_UPDATE: "a static graph cannot receive new data (nodes or links).\
Make sure config.staticGraph is set to true if you want to update graph data",
INVALID_LINKS: "you provided a invalid links data structure.\
Links source and target attributes must point to an existent node",
INSUFFICIENT_DATA: "you have not provided enough data for react-d3-graph to render something.\
You need to provide at least one node"
STATIC_GRAPH_DATA_UPDATE: "a static graph cannot receive new data (nodes or links). Make sure config.staticGraph is set to true if you want to update graph data",
INVALID_LINKS: "you provided a invalid links data structure. Links source and target attributes must point to an existent node",
INSUFFICIENT_DATA: "you have not provided enough data for react-d3-graph to render something. You need to provide at least one node"
};
4 changes: 4 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ function isObjectEmpty(o) {
function merge(o1={}, o2={}, _depth=0) {
let o = {};

if (Object.keys(o1 || {}).length === 0) {
return (o2 && !isObjectEmpty(o2)) ? o2 : {};
}

for (let k of Object.keys(o1)) {
const nestedO = !!(o2[k] && typeof o2[k] === 'object' && typeof o1[k] === 'object' && _depth < MAX_DEPTH);

Expand Down
Loading

0 comments on commit 72efff6

Please sign in to comment.