Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/tests coverage #44

Merged
merged 18 commits into from
Dec 2, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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