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

Refactor/separation of concerns render + logic #49

Merged
merged 8 commits into from
Jan 29, 2018
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
4 changes: 3 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
};
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,5 @@ coverage
sandbox/rd3g.sandbox.bundle.js.map
gen-docs
.DS_Store
.vscode/
**/.DS_Store
.vscode
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-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*
Expand Down
356 changes: 356 additions & 0 deletions src/components/graph/graph.helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,356 @@
/**
* @module Graph/helper
* @description
* Offers a series of methods that isolate logic of Graph component and also from Graph rendering methods.
*/
/**
* @typedef {Object} Link
* @property {string} source - the node id of the source in the link.
* @property {string} target - the node id of the target in the link.
* @memberof Graph/helper
*/
/**
* @typedef {Object} Node
* @property {string} id - the id of the node.
* @property {string} [color] - color of the node (optional).
* @property {string} [size] - size of the node (optional).
* @property {string} [symbolType] - symbol type of the node (optional).
* @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.<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.
* @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 {Array.<Link>} graphLinks - an array of all graph links.
* @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 {Array.<Node>} graphNodes - the array of nodes provided by the rd3g consumer.
* @returns {Object.<string, Object>} returns the nodes ready to be used within rd3g with additional properties such as x, y
* and highlighted values.
* @memberof Graph/helper
*/
function _initializeNodes(graphNodes) {
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
* @returns {undefined}
*/
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.<string, Object>} nodes - same as {@link #buildGraph|nodes in buildGraph}.
* @param {Object.<string, 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
};
Loading