react-d3-graph
Advanced tools
Comparing version 2.0.0-rc1 to 2.0.0-rc2
# Change Log | ||
## [2.0.0-rc2](https://github.com/danielcaldas/react-d3-graph/tree/2.0.0-rc2) | ||
[Full Changelog](https://github.com/danielcaldas/react-d3-graph/compare/2.0.0-rc1...2.0.0-rc2) | ||
**Implemented enhancements:** | ||
* Allow nodes to override strokeColor [\#122](https://github.com/danielcaldas/react-d3-graph/issues/122) | ||
**Fixed bugs:** | ||
* Custom onNodeClick handler not triggering on collapsible nodes [\#136](https://github.com/danielcaldas/react-d3-graph/issues/136) | ||
* 🐛 Global `viewGenerator` not been applied to the nodes [\#130](https://github.com/danielcaldas/react-d3-graph/issues/130) | ||
**Closed issues:** | ||
* graph constantly re-rendering even when app is idle? [\#145](https://github.com/danielcaldas/react-d3-graph/issues/145) | ||
* Allow users to pass in a function in node.labelProperty [\#133](https://github.com/danielcaldas/react-d3-graph/issues/133) | ||
* Drop yarn support for development \(stick to npm only\) [\#127](https://github.com/danielcaldas/react-d3-graph/issues/127) | ||
* Link mouse cursor property [\#119](https://github.com/danielcaldas/react-d3-graph/issues/119) | ||
* Center graph on a specific node [\#102](https://github.com/danielcaldas/react-d3-graph/issues/102) | ||
* Links with directional arrow [\#88](https://github.com/danielcaldas/react-d3-graph/issues/88) | ||
**Merged pull requests:** | ||
* Fix/right clicks [\#140](https://github.com/danielcaldas/react-d3-graph/pull/140) ([danielcaldas](https://github.com/danielcaldas)) | ||
* Refactor/clean link component [\#139](https://github.com/danielcaldas/react-d3-graph/pull/139) ([danielcaldas](https://github.com/danielcaldas)) | ||
* fix: Trigger custom click handler in collapsible nodes [\#137](https://github.com/danielcaldas/react-d3-graph/pull/137) ([LonelyPrincess](https://github.com/LonelyPrincess)) | ||
* Add Support to pass a function to node.labelProperty [\#135](https://github.com/danielcaldas/react-d3-graph/pull/135) ([dgautsch](https://github.com/dgautsch)) | ||
* Support Development on Windows Machines [\#134](https://github.com/danielcaldas/react-d3-graph/pull/134) ([dgautsch](https://github.com/dgautsch)) | ||
* Feature/directional graph [\#132](https://github.com/danielcaldas/react-d3-graph/pull/132) ([danielcaldas](https://github.com/danielcaldas)) | ||
* Global `viewGenerator` included in default config object [\#131](https://github.com/danielcaldas/react-d3-graph/pull/131) ([LonelyPrincess](https://github.com/LonelyPrincess)) | ||
* Remove Yarn [\#128](https://github.com/danielcaldas/react-d3-graph/pull/128) ([sasalx](https://github.com/sasalx)) | ||
* Feature/right clicking [\#124](https://github.com/danielcaldas/react-d3-graph/pull/124) ([ghardin137](https://github.com/ghardin137)) | ||
* Allow nodes to override strokeColor [\#123](https://github.com/danielcaldas/react-d3-graph/pull/123) ([Andras-Simon](https://github.com/Andras-Simon)) | ||
* fix: \#119 Add mouseCursor prop to \<Link\> [\#120](https://github.com/danielcaldas/react-d3-graph/pull/120) ([kaungmyatlwin](https://github.com/kaungmyatlwin)) | ||
* Add onClick handler to the canvas, for use in eg. unselecting nodes [\#113](https://github.com/danielcaldas/react-d3-graph/pull/113) ([smilykoch](https://github.com/smilykoch)) | ||
* Focus view on a node [\#107](https://github.com/danielcaldas/react-d3-graph/pull/107) ([LonelyPrincess](https://github.com/LonelyPrincess)) | ||
## [2.0.0-rc1](https://github.com/danielcaldas/react-d3-graph/tree/2.0.0-rc1) | ||
@@ -4,0 +42,0 @@ |
@@ -45,6 +45,11 @@ 'use strict'; | ||
* rearrange all nodes positions based on new position of dragged node (note: **staticGraph** should be false). | ||
* @param {boolean} [collapsible=false] - 🚅🚅🚅 Allow leaf neighbours nodes to be collapsed (folded), this will allow users to clear the way out and focus on the parts of the graph that really matter. | ||
* To see an example of this behavior you can access this sandbox link that has a specific set up to experiment this feature. | ||
* @param {boolean} [collapsible=false] - 🚅🚅🚅 Allow leaf neighbors nodes to be collapsed (folded), this will allow users to clear the way out and focus on the parts of the graph that really matter. | ||
* To see an example of this behavior you can access this sandbox link that has a specific set up to experiment this feature. **NOTE**: At this moment | ||
* nodes without connections (orphan nodes) are not rendered when this property is activated (see [react-d3-graph/issues/#129](https://github.com/danielcaldas/react-d3-graph/issues/129)). | ||
* <br/> | ||
* <img src="https://github.com/danielcaldas/react-d3-graph/blob/master/docs/rd3g-collapsible.gif?raw=true" width="820" height="480"/> | ||
* @param {boolean} [directed=false] - This property makes react-d3-graph handle your graph as a directed graph. It will | ||
* out of the box provide the look and feel of a directed graph and add directional semantic to links. You can see a sample in the image below. | ||
* <br/> | ||
* <img src="https://github.com/danielcaldas/react-d3-graph/blob/master/docs/rd3g-directed.gif?raw=true" width="820" height="480"/> | ||
* @param {number} [height=400] - the height of the (svg) area where the graph will be rendered. | ||
@@ -63,2 +68,18 @@ * @param {boolean} [nodeHighlightBehavior=false] - 🚅🚅🚅 when user mouse hovers a node that node and adjacent common | ||
* @param {number} [minZoom=0.1] - min zoom that can be performed against the graph. | ||
* @param {number} [focusZoom=1] - zoom that will be applied when the graph view is focused in a node. Its value must be between | ||
* *minZoom* and *maxZoom*. If the specified *focusZoom* is out of this range, *minZoom* or *maxZoom* will be applied instead. | ||
* **NOTE:** This animation is not trigger by default. In order to trigger it you need to pass down to `react-d3-graph` the | ||
* node that you want to focus via prop `focusedNodeId` along side with nodes and links: | ||
* | ||
* ```javascript | ||
* const data = { | ||
* nodes: this.state.data.nodes, | ||
* links: this.state.data.links, | ||
* focusedNodeId: 'nodeIdToTriggerZoomAnimation' | ||
* }; | ||
* ``` | ||
* | ||
* <img src="https://github.com/danielcaldas/react-d3-graph/blob/master/docs/rd3g-zoom-animation.gif?raw=true" width="820" height="480"/> | ||
* | ||
* @param {number} [focusAnimationDuration=0.75] - duration (in seconds) for the animation that takes place when focusing the graph on a node. | ||
* @param {boolean} [panAndZoom=false] - 🚅🚅🚅 pan and zoom effect when performing zoom in the graph, | ||
@@ -97,5 +118,15 @@ * a similar functionality may be consulted {@link https://bl.ocks.org/mbostock/2a39a768b1d4bc00a09650edef75ad39|here}. | ||
* @param {number} [node.highlightStrokeWidth=1.5] - strokeWidth in highlighted state. | ||
* @param {string} [node.labelProperty='id'] - this is the node property that will be used in runtime to | ||
* @param {string|Function} [node.labelProperty='id'] - this is the node property that will be used in runtime to | ||
* fetch the label content. You just need to add some property (e.g. firstName) to the node payload and then set | ||
* node.labelProperty to be **'firstName'**. | ||
* node.labelProperty to be **'firstName'**. **This can also be a function!**, if you pass a function here it will be called | ||
* to obtain the `label` value on the fly, as a client you will receive all the node information that you passed down into react-d3-graph, | ||
* so the signature of the function would be: | ||
* ```javascript | ||
* function myCustomLabelBuilder(node) { | ||
* // do stuff to get the final result... | ||
* return 'label string'; | ||
* } | ||
* ``` | ||
* Then you just need to make sure that you pass this function in the config in `config.node.labelProperty`. | ||
* <br/> | ||
* @param {string} [node.mouseCursor='pointer'] - {@link https://developer.mozilla.org/en/docs/Web/CSS/cursor?v=control|cursor} | ||
@@ -107,3 +138,3 @@ * property for when some node is mouse hovered. | ||
* @param {number} [node.size=200] - 🔍🔍🔍 defines the size of all nodes. | ||
* @param {string} [node.strokeColor='none'] - color for the stroke of each node. | ||
* @param {string} [node.strokeColor='none'] - 🔍🔍🔍 this is the stroke color that will be applied to the node if no **strokeColor property** is found inside the node itself (yes **you can pass a property 'strokeColor' inside the node and that stroke color will override this default one** ). | ||
* @param {number} [node.strokeWidth=1.5] - the width of the all node strokes. | ||
@@ -125,3 +156,3 @@ * @param {string} [node.svg=''] - 🔍🔍🔍 render custom svg for nodes in alternative to **node.symbolType**. This svg can | ||
* **[note]** react-d3-graph will map this values to [d3 symbols](https://github.com/d3/d3-shape#symbols) | ||
* @param {Function} [node.viewGenerator=undefined] - 🔍🔍🔍 function that receives a node and returns a JSX view. | ||
* @param {Function} [node.viewGenerator=null] - 🔍🔍🔍 function that receives a node and returns a JSX view. | ||
* <br/> | ||
@@ -132,2 +163,6 @@ * @param {Object} link link object is explained in the next section. ⬇️ | ||
* (from version 1.3.0 this property can be configured at link level). | ||
* @param {string} [link.highlightColor='#d3d3d3'] - links' color in highlight state. | ||
* <img src="https://github.com/danielcaldas/react-d3-graph/blob/master/docs/rd3g-bend.gif?raw=true" width="820" height="480"/> | ||
* @param {string} [link.mouseCursor='pointer'] - {@link https://developer.mozilla.org/en/docs/Web/CSS/cursor?v=control|cursor} | ||
* property for when link is mouse hovered. | ||
* @param {number} [link.opacity=1] - the default opacity value for links. | ||
@@ -145,3 +180,2 @@ * @param {boolean} [link.semanticStrokeWidth=false] - when set to true all links will have | ||
* ``` | ||
* @param {string} [link.highlightColor='#d3d3d3'] - links' color in highlight state. | ||
* @param {string} [link.type='STRAIGHT'] - the type of line to draw, available types at this point are: | ||
@@ -152,3 +186,2 @@ * - "STRAIGHT" <small>(default)</small> - a straight line. | ||
* <br/> | ||
* <img src="https://github.com/danielcaldas/react-d3-graph/blob/master/docs/rd3g-bend.gif?raw=true" width="820" height="480"/> | ||
* | ||
@@ -174,2 +207,3 @@ * @example | ||
collapsible: false, | ||
directed: false, | ||
height: 400, | ||
@@ -181,2 +215,4 @@ highlightDegree: 1, | ||
minZoom: 0.1, | ||
focusZoom: 1, | ||
focusAnimationDuration: 0.75, | ||
nodeHighlightBehavior: false, | ||
@@ -210,3 +246,4 @@ panAndZoom: false, | ||
svg: '', | ||
symbolType: 'circle' | ||
symbolType: 'circle', | ||
viewGenerator: null | ||
}, | ||
@@ -216,2 +253,3 @@ link: { | ||
highlightColor: '#d3d3d3', | ||
mouseCursor: 'pointer', | ||
opacity: 1, | ||
@@ -218,0 +256,0 @@ semanticStrokeWidth: false, |
@@ -6,11 +6,10 @@ 'use strict'; | ||
}); | ||
exports.updateNodeHighlightedValue = exports.toggleNodeConnection = exports.initializeGraphState = exports.getNodeCardinality = exports.getLeafNodeConnections = exports.disconnectLeafNodeConnections = exports.checkForGraphElementsChanges = exports.checkForGraphConfigChanges = exports.buildNodeProps = exports.buildLinkProps = undefined; | ||
exports.getCenterAndZoomTransformation = exports.updateNodeHighlightedValue = exports.initializeGraphState = exports.checkForGraphElementsChanges = exports.checkForGraphConfigChanges = exports.buildNodeProps = exports.buildLinkProps = undefined; | ||
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; /*eslint-disable max-lines*/ | ||
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; /** | ||
* @module Graph/helper | ||
* @description | ||
* Offers a series of methods that isolate logic of Graph component and also from Graph rendering methods. | ||
*/ | ||
/** | ||
* @module Graph/helper | ||
* @description | ||
* Offers a series of methods that isolate logic of Graph component and also from Graph rendering methods. | ||
*/ | ||
/** | ||
* @typedef {Object} Link | ||
@@ -53,2 +52,4 @@ * @property {string} source - the node id of the source in the link. | ||
var _marker = require('../marker/marker.helper'); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -109,2 +110,3 @@ | ||
* @param {Array.<Link>} graphLinks - an array of all graph links. | ||
* @param {Object} config - the graph config. | ||
* @returns {Object.<string, Object>} an object containing a matrix of connections of the graph, for each nodeId, | ||
@@ -114,3 +116,3 @@ * there is an object that maps adjacent nodes ids (string) and their values (number). | ||
*/ | ||
function _initializeLinks(graphLinks) { | ||
function _initializeLinks(graphLinks, config) { | ||
return graphLinks.reduce(function (links, l) { | ||
@@ -128,5 +130,10 @@ var source = l.source.id || l.source; | ||
// TODO: If the graph is directed this should be adapted | ||
links[source][target] = links[target][source] = l.value || 1; | ||
var value = config.collapsible && l.isHidden ? 0 : l.value || 1; | ||
links[source][target] = value; | ||
if (!config.directed) { | ||
links[target][source] = value; | ||
} | ||
return links; | ||
@@ -174,6 +181,11 @@ }, {}); | ||
* @param {Array.<Object>} d3Links - all d3Links. | ||
* @param {Object} 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 d3Link. | ||
* @memberof Graph/helper | ||
*/ | ||
function _mapDataLinkToD3Link(link, index) { | ||
var d3Links = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; | ||
var config = arguments[3]; | ||
var state = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {}; | ||
@@ -183,3 +195,11 @@ var d3Link = d3Links[index]; | ||
if (d3Link) { | ||
return d3Link; | ||
var toggledDirected = state.config && state.config.directed && config.directed !== state.config.directed; | ||
// every time we toggle directed config all links should be visible again | ||
if (toggledDirected) { | ||
return _extends({}, d3Link, { isHidden: false }); | ||
} | ||
// every time we disable collapsible (collapsible is false) all links should be visible again | ||
return config.collapsible ? d3Link : _extends({}, d3Link, { isHidden: false }); | ||
} | ||
@@ -208,3 +228,2 @@ | ||
* @param {Object} data - Same as {@link #initializeGraphState|data in initializeGraphState}. | ||
* @memberof Graph/helper | ||
* @throws can throw the following error msg: | ||
@@ -214,2 +233,3 @@ * INSUFFICIENT_DATA - msg if no nodes are provided | ||
* @returns {undefined} | ||
* @memberof Graph/helper | ||
*/ | ||
@@ -305,15 +325,16 @@ function _validateGraphData(data) { | ||
var markerId = config.directed ? (0, _marker.getMarkerId)(highlight, transform, config) : null; | ||
return { | ||
markerId: markerId, | ||
d: d, | ||
source: source, | ||
target: target, | ||
x1: x1, | ||
y1: y1, | ||
x2: x2, | ||
y2: y2, | ||
strokeWidth: strokeWidth, | ||
stroke: stroke, | ||
mouseCursor: config.link.mouseCursor, | ||
className: _graph3.default.LINK_CLASS_NAME, | ||
opacity: opacity, | ||
onClickLink: linkCallbacks.onClickLink, | ||
onRightClickLink: linkCallbacks.onRightClickLink, | ||
onMouseOverLink: linkCallbacks.onMouseOverLink, | ||
@@ -349,3 +370,3 @@ onMouseOutLink: linkCallbacks.onMouseOutLink | ||
var stroke = config.node.strokeColor; | ||
var stroke = node.strokeColor || config.node.strokeColor; | ||
@@ -356,2 +377,8 @@ if (highlight && config.node.highlightStrokeColor !== _graph3.default.KEYWORDS.SAME) { | ||
var label = node[config.node.labelProperty] || node.id; | ||
if (typeof config.node.labelProperty === 'function') { | ||
label = config.node.labelProperty(node); | ||
} | ||
var t = 1 / transform; | ||
@@ -376,4 +403,5 @@ var nodeSize = node.size || config.node.size; | ||
id: node.id, | ||
label: node[config.node.labelProperty] || node.id, | ||
label: label, | ||
onClickNode: nodeCallbacks.onClickNode, | ||
onRightClickNode: nodeCallbacks.onRightClickNode, | ||
onMouseOverNode: nodeCallbacks.onMouseOverNode, | ||
@@ -407,2 +435,3 @@ onMouseOut: nodeCallbacks.onMouseOut, | ||
* some node or link or was updated). | ||
* @memberof Graph/helper | ||
*/ | ||
@@ -447,6 +476,7 @@ function checkForGraphElementsChanges(nextProps, currentState) { | ||
* - d3ConfigUpdated - specific flag that indicates changes in d3 configurations. | ||
* @memberof Graph/helper | ||
*/ | ||
function checkForGraphConfigChanges(nextProps, currentState) { | ||
var newConfig = nextProps.config || {}; | ||
var configUpdated = newConfig && !_utils2.default.isObjectEmpty(newConfig) && !_utils2.default.isDeepEqual(newConfig, currentState.config); | ||
var configUpdated = newConfig && !_utils2.default.isEmptyObject(newConfig) && !_utils2.default.isDeepEqual(newConfig, currentState.config); | ||
var d3ConfigUpdated = newConfig && newConfig.d3 && !_utils2.default.isDeepEqual(newConfig.d3, currentState.config.d3); | ||
@@ -482,3 +512,3 @@ | ||
links: data.links.map(function (l, index) { | ||
return _mapDataLinkToD3Link(l, index, state && state.d3Links); | ||
return _mapDataLinkToD3Link(l, index, state && state.d3Links, config, state); | ||
}) | ||
@@ -499,3 +529,3 @@ }; | ||
var nodes = _initializeNodes(graph.nodes); | ||
var links = _initializeLinks(graph.links); // matrix of graph connections | ||
var links = _initializeLinks(graph.links, newConfig); // matrix of graph connections | ||
var _graph = graph, | ||
@@ -508,2 +538,13 @@ d3Nodes = _graph.nodes, | ||
var minZoom = newConfig.minZoom, | ||
maxZoom = newConfig.maxZoom, | ||
focusZoom = newConfig.focusZoom; | ||
if (focusZoom > maxZoom) { | ||
newConfig.focusZoom = maxZoom; | ||
} else if (focusZoom < minZoom) { | ||
newConfig.focusZoom = minZoom; | ||
} | ||
return { | ||
@@ -558,95 +599,22 @@ id: formatedId, | ||
/** | ||
* This function disconnects all the connections from leaf -> parent. | ||
* @param {string} targetNodeId - The id of the node from which to disconnect the leaf nodes | ||
* @param {Object.<string, number>} originalConnections - An object containing a matrix of connections of the nodes. | ||
* @param {Array} d3Links - An array containing all the d3 links. | ||
* @returns {Object.<string, number>} - Contains the new links and d3Links. | ||
* Returns the transformation to apply in order to center the graph on the | ||
* selected node. | ||
* @param {Object} d3Node - node to focus the graph view on. | ||
* @param {Object} config - same as {@link #buildGraph|config in buildGraph}. | ||
* @returns {string} transform rule to apply. | ||
* @memberof Graph/helper | ||
*/ | ||
function disconnectLeafNodeConnections(targetNodeId, originalConnections, d3Links) { | ||
var leafNodesToToggle = getLeafNodeConnections(targetNodeId, originalConnections); | ||
var toggledLeafNodes = Object.keys(leafNodesToToggle).reduce(function (newLeafNodeConnections, leafNodeId) { | ||
// toggle connections from Leaf node to Parent node | ||
newLeafNodeConnections[leafNodeId] = toggleNodeConnection(targetNodeId, originalConnections[leafNodeId]); | ||
function getCenterAndZoomTransformation(d3Node, config) { | ||
if (!d3Node) { | ||
return; | ||
} | ||
return newLeafNodeConnections; | ||
}, {}); | ||
var width = config.width, | ||
height = config.height, | ||
focusZoom = config.focusZoom; | ||
var toggledLeafNodesList = Object.keys(toggledLeafNodes); | ||
var toggledD3Links = d3Links.reduce(function (allD3Links, currentD3Link) { | ||
var source = currentD3Link.source, | ||
target = currentD3Link.target; | ||
var isLeafNode = toggledLeafNodesList.some(function (leafNodeId) { | ||
return leafNodeId === '' + source.id || leafNodeId === '' + target.id; | ||
}); | ||
isLeafNode ? allD3Links.push(_extends({}, currentD3Link, { isHidden: !currentD3Link.isHidden })) : allD3Links.push(currentD3Link); | ||
return allD3Links; | ||
}, []); | ||
return { | ||
d3Links: toggledD3Links, | ||
links: _extends({}, originalConnections, toggledLeafNodes) | ||
}; | ||
return '\n translate(' + width / 2 + ', ' + height / 2 + ')\n scale(' + focusZoom + ')\n translate(' + -d3Node.x + ', ' + -d3Node.y + ')\n '; | ||
} | ||
/** | ||
* This function toggles the value for a given node connection (1 -> 0 and vice-versa). | ||
* @param {string} targetNodeId - The id of the node which to toggle | ||
* @param {Object.<string, number>} allConnections - An object containing a matrix of connections of the node | ||
* where we want to toggle the connection (destinations/targets). | ||
* @returns {Object.<string, number>} - Contains the new connections with the target node toggled. | ||
* @memberof Graph/helper | ||
*/ | ||
function toggleNodeConnection(targetNodeId, allConnections) { | ||
var newConnection = _defineProperty({}, targetNodeId, allConnections[targetNodeId] === 1 ? 0 : 1); | ||
return _extends({}, allConnections, newConnection); | ||
} | ||
/** | ||
* Based on a starting node (ID) and all the current connections between all the nodes. | ||
* Find the leaf node connections of that starting node. | ||
* @param {string} startingNodeId - The id of the node where the "search" should be started. | ||
* @param {Object.<string, number>} currentConnections - An object containing a matrix of connections of the nodes. | ||
* @returns {Object.<string, number>} - Contains the connections to leaf nodes based on the given starting node. | ||
* @memberof Graph/helper | ||
*/ | ||
function getLeafNodeConnections(startingNodeId, currentConnections) { | ||
var startingNodeConnections = currentConnections[startingNodeId]; | ||
var startingNodeConnectionsList = Object.keys(startingNodeConnections); | ||
return startingNodeConnectionsList.reduce(function (allLeafNodes, candidateLeafId) { | ||
var candidateLeafConnections = currentConnections[candidateLeafId]; | ||
var candidateLeafConnectionList = Object.keys(candidateLeafConnections); | ||
var isLeafNode = candidateLeafConnectionList.length === 1; | ||
if (isLeafNode) { | ||
allLeafNodes[candidateLeafId] = candidateLeafConnections; | ||
} | ||
return allLeafNodes; | ||
}, {}); | ||
} | ||
/** | ||
* Given a node and the connections matrix, give the cardinality of the node. | ||
* | ||
* i.e.: Taking into account the node is connected to nothing, it amounts to 0. | ||
* Being connected to three nodes, it amounts to 3. | ||
* @param {string} nodeId - The id of the node to get the cardinality of | ||
* @param {Object.<string, number>} linksMatrix - An object containing a matrix of connections of the nodes. | ||
* @returns {number} - Contains the cardinality of the asked node. | ||
* @memberof Graph/helper | ||
*/ | ||
function getNodeCardinality(nodeId, linksMatrix) { | ||
var nodeConnectivityList = Object.values(linksMatrix[nodeId] || []); | ||
return nodeConnectivityList.reduce(function (cardinality, nodeConnectivity) { | ||
return cardinality + nodeConnectivity; | ||
}, 0); | ||
} | ||
exports.buildLinkProps = buildLinkProps; | ||
@@ -656,7 +624,4 @@ exports.buildNodeProps = buildNodeProps; | ||
exports.checkForGraphElementsChanges = checkForGraphElementsChanges; | ||
exports.disconnectLeafNodeConnections = disconnectLeafNodeConnections; | ||
exports.getLeafNodeConnections = getLeafNodeConnections; | ||
exports.getNodeCardinality = getNodeCardinality; | ||
exports.initializeGraphState = initializeGraphState; | ||
exports.toggleNodeConnection = toggleNodeConnection; | ||
exports.updateNodeHighlightedValue = updateNodeHighlightedValue; | ||
exports.updateNodeHighlightedValue = updateNodeHighlightedValue; | ||
exports.getCenterAndZoomTransformation = getCenterAndZoomTransformation; |
@@ -35,10 +35,14 @@ 'use strict'; | ||
var _graph5 = require('./graph.renderer'); | ||
var _collapse = require('./collapse.helper'); | ||
var graphRenderer = _interopRequireWildcard(_graph5); | ||
var collapseHelper = _interopRequireWildcard(_collapse); | ||
var _graph6 = require('./graph.helper'); | ||
var _graph5 = require('./graph.helper'); | ||
var graphHelper = _interopRequireWildcard(_graph6); | ||
var graphHelper = _interopRequireWildcard(_graph5); | ||
var _graph6 = require('./graph.renderer'); | ||
var graphRenderer = _interopRequireWildcard(_graph6); | ||
var _utils = require('../../utils'); | ||
@@ -94,2 +98,6 @@ | ||
* // graph event callbacks | ||
* const onClickGraph = function() { | ||
* window.alert('Clicked the graph background'); | ||
* }; | ||
* | ||
* const onClickNode = function(nodeId) { | ||
@@ -99,2 +107,6 @@ * window.alert('Clicked node ${nodeId}'); | ||
* | ||
* const onRightClickNode = function(event, nodeId) { | ||
* window.alert('Right clicked node ${nodeId}'); | ||
* }; | ||
* | ||
* const onMouseOverNode = function(nodeId) { | ||
@@ -112,2 +124,6 @@ * window.alert(`Mouse over node ${nodeId}`); | ||
* | ||
* const onRightClickLink = function(event, source, target) { | ||
* window.alert('Right clicked link between ${source} and ${target}'); | ||
* }; | ||
* | ||
* const onMouseOverLink = function(source, target) { | ||
@@ -125,4 +141,7 @@ * window.alert(`Mouse over in link between ${source} and ${target}`); | ||
* config={myConfig} | ||
* onClickGraph={onClickGraph} | ||
* onClickNode={onClickNode} | ||
* onRightClickNode={onRightClickNode} | ||
* onClickLink={onClickLink} | ||
* onRightClickLink={onRightClickLink} | ||
* onMouseOverNode={onMouseOverNode} | ||
@@ -192,2 +211,3 @@ * onMouseOutNode={onMouseOutNode} | ||
* @param {Object} state - new state to pass on. | ||
* @param {Function} [cb] - optional callback to fed in to {@link setState()|https://reactjs.org/docs/react-component.html#setstate}. | ||
* @returns {undefined} | ||
@@ -291,3 +311,6 @@ */ | ||
_this._onDragStart = function () { | ||
return _this.pauseSimulation(); | ||
_this.pauseSimulation(); | ||
if (_this.state.enableFocusAnimation) { | ||
_this.setState({ enableFocusAnimation: false }); | ||
} | ||
}; | ||
@@ -302,3 +325,4 @@ | ||
var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
return _this.setState(state); | ||
var cb = arguments[1]; | ||
return cb ? _this.setState(state, cb) : _this.setState(state); | ||
}; | ||
@@ -377,10 +401,53 @@ | ||
if (_this.state.config.collapsible) { | ||
var disconnectedLeafNodesPartialState = graphHelper.disconnectLeafNodeConnections(clickedNodeId, _this.state.links, _this.state.d3Links); | ||
var leafConnections = collapseHelper.getTargetLeafConnections(clickedNodeId, _this.state.links, _this.state.config); | ||
var links = collapseHelper.toggleLinksMatrixConnections(_this.state.links, leafConnections, _this.state.config); | ||
var d3Links = collapseHelper.toggleLinksConnections(_this.state.d3Links, links); | ||
_this._tick(_extends({}, disconnectedLeafNodesPartialState)); | ||
_this._tick({ | ||
links: links, | ||
d3Links: d3Links | ||
}, function () { | ||
return _this.props.onClickNode && _this.props.onClickNode(clickedNodeId); | ||
}); | ||
} else { | ||
_this.props.onClickNode && _this.props.onClickNode(clickedNodeId); | ||
} | ||
}; | ||
_this.props.onClickNode && _this.props.onClickNode(clickedNodeId); | ||
_this.onClickGraph = function (e) { | ||
if (_this.state.enableFocusAnimation) { | ||
_this.setState({ enableFocusAnimation: false }); | ||
} | ||
// Only trigger the graph onClickHandler, if not clicked a node or link. | ||
// toUpperCase() is added as a precaution, as the documentation says tagName should always | ||
// return in UPPERCASE, but chrome returns lowercase | ||
if (e.target.tagName.toUpperCase() === 'SVG' && e.target.attributes.name.value === 'svg-container-' + _this.state.id) { | ||
_this.props.onClickGraph && _this.props.onClickGraph(); | ||
} | ||
}; | ||
_this._generateFocusAnimationProps = function () { | ||
var focusedNodeId = _this.state.focusedNodeId; | ||
// In case an older animation was still not complete, clear previous timeout to ensure the new one is not cancelled | ||
if (_this.state.enableFocusAnimation) { | ||
if (_this.focusAnimationTimeout) { | ||
clearTimeout(_this.focusAnimationTimeout); | ||
} | ||
_this.focusAnimationTimeout = setTimeout(function () { | ||
return _this.setState({ enableFocusAnimation: false }); | ||
}, _this.state.config.focusAnimationDuration * 1000); | ||
} | ||
var transitionDuration = _this.state.enableFocusAnimation ? _this.state.config.focusAnimationDuration : 0; | ||
return { | ||
style: { transitionDuration: transitionDuration + 's' }, | ||
transform: focusedNodeId ? _this.state.focusTransformation : null | ||
}; | ||
}; | ||
if (!_this.props.id) { | ||
@@ -390,2 +457,3 @@ _utils2.default.throwErr(_this.constructor.name, _err2.default.GRAPH_NO_ID_PROP); | ||
_this.focusAnimationTimeout = null; | ||
_this.state = graphHelper.initializeGraphState(_this.props, _this.state); | ||
@@ -428,2 +496,9 @@ return _this; | ||
var focusedNodeId = nextProps.data.focusedNodeId; | ||
var d3FocusedNode = this.state.d3Nodes.find(function (node) { | ||
return '' + node.id === '' + focusedNodeId; | ||
}); | ||
var focusTransformation = graphHelper.getCenterAndZoomTransformation(d3FocusedNode, this.state.config); | ||
var enableFocusAnimation = this.props.data.focusedNodeId !== nextProps.data.focusedNodeId; | ||
this.setState(_extends({}, state, { | ||
@@ -434,3 +509,6 @@ config: config, | ||
newGraphElements: newGraphElements, | ||
transform: transform | ||
transform: transform, | ||
focusedNodeId: focusedNodeId, | ||
enableFocusAnimation: enableFocusAnimation, | ||
focusTransformation: focusTransformation | ||
})); | ||
@@ -477,2 +555,17 @@ } | ||
/** | ||
* Calls the callback passed to the component. | ||
* @param {Object} e - The event of onClick handler. | ||
* @returns {undefined} | ||
*/ | ||
/** | ||
* Obtain a set of properties which will be used to perform the focus and zoom animation if | ||
* required. In case there's not a focus and zoom animation in progress, it should reset the | ||
* transition duration to zero and clear transformation styles. | ||
* @returns {Object} - Focus and zoom animation properties. | ||
*/ | ||
}, { | ||
@@ -483,2 +576,3 @@ key: 'render', | ||
onClickNode: this.onClickNode, | ||
onRightClickNode: this.props.onRightClickNode, | ||
onMouseOverNode: this.onMouseOverNode, | ||
@@ -488,2 +582,3 @@ onMouseOut: this.onMouseOutNode | ||
onClickLink: this.props.onClickLink, | ||
onRightClickLink: this.props.onRightClickLink, | ||
onMouseOverLink: this.onMouseOverLink, | ||
@@ -493,3 +588,4 @@ onMouseOutLink: this.onMouseOutLink | ||
nodes = _graphRenderer$buildG.nodes, | ||
links = _graphRenderer$buildG.links; | ||
links = _graphRenderer$buildG.links, | ||
defs = _graphRenderer$buildG.defs; | ||
@@ -501,2 +597,4 @@ var svgStyle = { | ||
var containerProps = this._generateFocusAnimationProps(); | ||
return _react2.default.createElement( | ||
@@ -507,6 +605,7 @@ 'div', | ||
'svg', | ||
{ style: svgStyle }, | ||
{ name: 'svg-container-' + this.state.id, style: svgStyle, onClick: this.onClickGraph }, | ||
defs, | ||
_react2.default.createElement( | ||
'g', | ||
{ id: this.state.id + '-' + _graph2.default.GRAPH_CONTAINER_ID }, | ||
_extends({ id: this.state.id + '-' + _graph2.default.GRAPH_CONTAINER_ID }, containerProps), | ||
links, | ||
@@ -513,0 +612,0 @@ nodes |
@@ -23,2 +23,4 @@ 'use strict'; | ||
var _marker = require('../marker/marker.const'); | ||
var _Link = require('../link/Link'); | ||
@@ -32,4 +34,10 @@ | ||
var _Marker = require('../marker/Marker'); | ||
var _Marker2 = _interopRequireDefault(_Marker); | ||
var _graph3 = require('./graph.helper'); | ||
var _collapse = require('./collapse.helper'); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -47,10 +55,16 @@ | ||
* @param {number} transform - value that indicates the amount of zoom transformation. | ||
* @returns {Object[]} returns the generated array of Link components. | ||
* @memberof Graph/renderer | ||
* @returns {Array.<Object>} returns the generated array of Link components. | ||
* @memberof Graph/helper | ||
*/ | ||
function _buildLinks(nodes, links, linksMatrix, config, linkCallbacks, highlightedNode, highlightedLink, transform) { | ||
return links.filter(function (_ref) { | ||
var isHidden = _ref.isHidden; | ||
return !isHidden; | ||
}).map(function (link) { | ||
var outLinks = links; | ||
if (config.collapsible) { | ||
outLinks = outLinks.filter(function (_ref) { | ||
var isHidden = _ref.isHidden; | ||
return !isHidden; | ||
}); | ||
} | ||
return outLinks.map(function (link) { | ||
var source = link.source, | ||
@@ -80,4 +94,4 @@ target = link.target; | ||
* @param {Object.<string, Object>} linksMatrix - the matrix of connections of the graph | ||
* @returns {Object} returns the generated array of nodes components | ||
* @memberof Graph/renderer | ||
* @returns {Array.<Object>} returns the generated array of node components | ||
* @memberof Graph/helper | ||
*/ | ||
@@ -89,3 +103,3 @@ function _buildNodes(nodes, nodeCallbacks, config, highlightedNode, highlightedLink, transform, linksMatrix) { | ||
outNodes = outNodes.filter(function (nodeId) { | ||
return (0, _graph3.getNodeCardinality)(nodeId, linksMatrix) > 0; | ||
return (0, _collapse.isNodeVisible)(nodeId, linksMatrix); | ||
}); | ||
@@ -102,2 +116,44 @@ } | ||
/** | ||
* Builds graph defs (for now markers, but we could also have gradients for instance). | ||
* NOTE: defs are static svg graphical objects, thus we only need to render them once, the result | ||
* is cached on the 1st call and from there we simply return the cached jsx. | ||
* @returns {Function} memoized build definitions function. | ||
* @memberof Graph/helper | ||
*/ | ||
function _buildDefs() { | ||
var cachedDefs = void 0; | ||
return function (config) { | ||
if (cachedDefs) { | ||
return cachedDefs; | ||
} | ||
var small = _marker.MARKER_SMALL_SIZE; | ||
var medium = small + _marker.MARKER_MEDIUM_OFFSET * config.maxZoom / 3; | ||
var large = small + _marker.MARKER_LARGE_OFFSET * config.maxZoom / 3; | ||
cachedDefs = _react2.default.createElement( | ||
'defs', | ||
null, | ||
_react2.default.createElement(_Marker2.default, { id: _marker.MARKERS.MARKER_S, refX: small, fill: config.link.color }), | ||
_react2.default.createElement(_Marker2.default, { id: _marker.MARKERS.MARKER_SH, refX: small, fill: config.link.highlightColor }), | ||
_react2.default.createElement(_Marker2.default, { id: _marker.MARKERS.MARKER_M, refX: medium, fill: config.link.color }), | ||
_react2.default.createElement(_Marker2.default, { id: _marker.MARKERS.MARKER_MH, refX: medium, fill: config.link.highlightColor }), | ||
_react2.default.createElement(_Marker2.default, { id: _marker.MARKERS.MARKER_L, refX: large, fill: config.link.color }), | ||
_react2.default.createElement(_Marker2.default, { id: _marker.MARKERS.MARKER_LH, refX: large, fill: config.link.highlightColor }) | ||
); | ||
return cachedDefs; | ||
}; | ||
} | ||
/** | ||
* Memoized reference for _buildDefs. | ||
* @param {Object} config - an object containing rd3g consumer defined configurations {@link #config config} for the graph. | ||
* @returns {Object} graph reusable objects [defs](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs). | ||
* @memberof Graph/helper | ||
*/ | ||
var _memoizedBuildDefs = _buildDefs(); | ||
/** | ||
* Method that actually is exported an consumed by Graph component in order to build all Nodes and Link | ||
@@ -146,3 +202,4 @@ * components. | ||
nodes: _buildNodes(nodes, nodeCallbacks, config, highlightedNode, highlightedLink, transform, linksMatrix), | ||
links: _buildLinks(nodes, links, linksMatrix, config, linkCallbacks, highlightedNode, highlightedLink, transform) | ||
links: _buildLinks(nodes, links, linksMatrix, config, linkCallbacks, highlightedNode, highlightedLink, transform), | ||
defs: _memoizedBuildDefs(config) | ||
}; | ||
@@ -149,0 +206,0 @@ } |
@@ -58,3 +58,3 @@ 'use strict'; | ||
* *CURVE_SMOOTH* type inspired by {@link http://bl.ocks.org/mbostock/1153292|mbostock - Mobile Patent Suits}. | ||
* @param {string} type type of curve to get radius strategy from. | ||
* @param {string} [type=LINE_TYPES.STRAIGHT] type of curve to get radius strategy from. | ||
* @returns {Function} a function that calculates a radius | ||
@@ -65,3 +65,3 @@ * to match curve type expectation. Fallback is the straight line. | ||
function getRadiusStrategy(type) { | ||
return RADIUS_STRATEGIES[type]; | ||
return RADIUS_STRATEGIES[type] || RADIUS_STRATEGIES[_link.LINE_TYPES.STRAIGHT]; | ||
} | ||
@@ -68,0 +68,0 @@ |
@@ -28,2 +28,6 @@ 'use strict'; | ||
* | ||
* const onRightClickLink = function(source, target) { | ||
* window.alert(`Right clicked link between ${source} and ${target}`); | ||
* }; | ||
* | ||
* const onMouseOverLink = function(source, target) { | ||
@@ -41,6 +45,3 @@ * window.alert(`Mouse over in link between ${source} and ${target}`); | ||
* target='idTargetNode' | ||
* x1=22 | ||
* y1=22 | ||
* x2=22 | ||
* y2=22 | ||
* markerId='marker-small' | ||
* strokeWidth=1.5 | ||
@@ -50,3 +51,5 @@ * stroke='green' | ||
* opacity=1 | ||
* mouseCursor='pointer' | ||
* onClickLink={onClickLink} | ||
* onRightClickLink={onRightClickLink} | ||
* onMouseOverLink={onMouseOverLink} | ||
@@ -71,2 +74,4 @@ * onMouseOutLink={onMouseOutLink} /> | ||
return _this.props.onClickLink && _this.props.onClickLink(_this.props.source, _this.props.target); | ||
}, _this.handleOnRightClickLink = function (event) { | ||
return _this.props.onRightClickLink && _this.props.onRightClickLink(event, _this.props.source, _this.props.target); | ||
}, _this.handleOnMouseOverLink = function () { | ||
@@ -85,2 +90,9 @@ return _this.props.onMouseOverLink && _this.props.onMouseOverLink(_this.props.source, _this.props.target); | ||
/** | ||
* Handle link right click event. | ||
* @param {Object} event - native event. | ||
* @returns {undefined} | ||
*/ | ||
/** | ||
* Handle mouse over link event. | ||
@@ -104,3 +116,4 @@ * @returns {undefined} | ||
opacity: this.props.opacity, | ||
fill: 'none' | ||
fill: 'none', | ||
cursor: this.props.mouseCursor | ||
}; | ||
@@ -112,11 +125,12 @@ | ||
onClick: this.handleOnClickLink, | ||
onContextMenu: this.handleOnRightClickLink, | ||
onMouseOut: this.handleOnMouseOutLink, | ||
onMouseOver: this.handleOnMouseOverLink, | ||
style: lineStyle, | ||
x1: this.props.x1, | ||
x2: this.props.x2, | ||
y1: this.props.y1, | ||
y2: this.props.y2 | ||
style: lineStyle | ||
}; | ||
if (this.props.markerId) { | ||
lineProps.markerEnd = 'url(#' + this.props.markerId + ')'; | ||
} | ||
return _react2.default.createElement('path', lineProps); | ||
@@ -123,0 +137,0 @@ } |
@@ -38,2 +38,6 @@ 'use strict'; | ||
* | ||
* const onRightClickNode = function(nodeId) { | ||
* window.alert('Right clicked node', nodeId); | ||
* } | ||
* | ||
* const onMouseOverNode = function(nodeId) { | ||
@@ -67,2 +71,3 @@ * window.alert('Mouse over node', nodeId); | ||
* onClickNode={onClickNode} | ||
* onRightClickNode={onRightClickNode} | ||
* onMouseOverNode={onMouseOverNode} | ||
@@ -87,2 +92,4 @@ * onMouseOutNode={onMouseOutNode} /> | ||
return _this.props.onClickNode && _this.props.onClickNode(_this.props.id); | ||
}, _this.handleOnRightClickNode = function (event) { | ||
return _this.props.onRightClickNode && _this.props.onRightClickNode(event, _this.props.id); | ||
}, _this.handleOnMouseOverNode = function () { | ||
@@ -101,2 +108,9 @@ return _this.props.onMouseOverNode && _this.props.onMouseOverNode(_this.props.id); | ||
/** | ||
* Handle right click on the node. | ||
* @param {Object} event - native event. | ||
* @returns {undefined} | ||
*/ | ||
/** | ||
* Handle mouse over node event. | ||
@@ -119,2 +133,3 @@ * @returns {undefined} | ||
onClick: this.handleOnClickNode, | ||
onContextMenu: this.handleOnRightClickNode, | ||
onMouseOut: this.handleOnMouseOutNode, | ||
@@ -121,0 +136,0 @@ onMouseOver: this.handleOnMouseOverNode, |
@@ -28,3 +28,3 @@ 'use strict'; | ||
function _isPropertyNestedObject(o, k) { | ||
return o.hasOwnProperty(k) && _typeof(o[k]) === 'object' && o[k] !== null && !isObjectEmpty(o[k]); | ||
return o.hasOwnProperty(k) && _typeof(o[k]) === 'object' && o[k] !== null && !isEmptyObject(o[k]); | ||
} | ||
@@ -49,3 +49,3 @@ | ||
if (isObjectEmpty(o1) && !isObjectEmpty(o2) || !isObjectEmpty(o1) && isObjectEmpty(o2)) { | ||
if (isEmptyObject(o1) && !isEmptyObject(o2) || !isEmptyObject(o1) && isEmptyObject(o2)) { | ||
return false; | ||
@@ -74,3 +74,3 @@ } | ||
} else { | ||
var r = isObjectEmpty(o1[k]) && isObjectEmpty(o2[k]) || o2.hasOwnProperty(k) && o2[k] === o1[k]; | ||
var r = isEmptyObject(o1[k]) && isEmptyObject(o2[k]) || o2.hasOwnProperty(k) && o2[k] === o1[k]; | ||
@@ -109,3 +109,3 @@ diffs.push(r); | ||
*/ | ||
function isObjectEmpty(o) { | ||
function isEmptyObject(o) { | ||
return !!o && (typeof o === 'undefined' ? 'undefined' : _typeof(o)) === 'object' && !Object.keys(o).length; | ||
@@ -133,3 +133,3 @@ } | ||
if (Object.keys(o1 || {}).length === 0) { | ||
return o2 && !isObjectEmpty(o2) ? o2 : {}; | ||
return o2 && !isEmptyObject(o2) ? o2 : {}; | ||
} | ||
@@ -202,2 +202,3 @@ | ||
* @returns {Object} the object resultant from the anti picking operation. | ||
* @memberof utils | ||
*/ | ||
@@ -229,3 +230,3 @@ function antiPick(o) { | ||
isDeepEqual: isDeepEqual, | ||
isObjectEmpty: isObjectEmpty, | ||
isEmptyObject: isEmptyObject, | ||
merge: merge, | ||
@@ -232,0 +233,0 @@ pick: pick, |
{ | ||
"name": "react-d3-graph", | ||
"version": "2.0.0-rc1", | ||
"version": "2.0.0-rc2", | ||
"description": "React component to build interactive and configurable graphs with d3 effortlessly", | ||
@@ -10,3 +10,3 @@ "author": "Daniel Caldas", | ||
"check:light": "npm run lint && npm run test", | ||
"dev": "NODE_ENV=dev webpack-dev-server --mode=development --content-base sandbox --config webpack.config.js --inline --hot --port 3002", | ||
"dev": "cross-env NODE_ENV=dev webpack-dev-server --mode=development --content-base sandbox --config webpack.config.js --inline --hot --port 3002", | ||
"dist:rd3g": "rm -rf dist/ && webpack --config webpack.config.dist.js -p --display-modules --optimize-minimize", | ||
@@ -17,4 +17,4 @@ "dist:sandbox": "webpack --config webpack.config.js -p", | ||
"docs:lint": "node_modules/documentation/bin/documentation.js lint src/**/*.js", | ||
"docs:watch": "node_modules/documentation/bin/documentation.js build src/**/*.js -f html -o gen-docs --watch", | ||
"docs": "npm run docs:lint && node_modules/documentation/bin/documentation.js build src/**/*.js -f html -o gen-docs && node_modules/documentation/bin/documentation.js build src/**/*.js -f md > gen-docs/DOCUMENTATION.md", | ||
"docs:watch": "node_modules/documentation/bin/documentation.js --config documentation.yml build src/**/*.js -f html -o gen-docs --watch", | ||
"docs": "npm run docs:lint && node_modules/documentation/bin/documentation.js --config documentation.yml build src/**/*.js -f html -o gen-docs && node_modules/documentation/bin/documentation.js build src/**/*.js -f md > gen-docs/DOCUMENTATION.md", | ||
"functional:local": "export CYPRESS_SANDBOX_URL=http://localhost:3002 && cypress open", | ||
@@ -30,3 +30,4 @@ "functional:remote": "export CYPRESS_SANDBOX_URL=https://danielcaldas.github.io/react-d3-graph/sandbox/index.html && cypress open", | ||
"test:watch": "jest --verbose --coverage --watchAll --config jest.config.js", | ||
"test": "jest --verbose --coverage --config jest.config.js" | ||
"test": "jest --verbose --coverage --config jest.config.js", | ||
"sandbox": "npm run dist:sandbox && npm run start" | ||
}, | ||
@@ -58,2 +59,3 @@ "lint-staged": { | ||
"babel-preset-stage-0": "6.24.1", | ||
"cross-env": "^5.2.0", | ||
"css-loader": "0.28.7", | ||
@@ -65,2 +67,3 @@ "cypress": "2.1.0", | ||
"eslint-config-recommended": "2.0.0", | ||
"eslint-plugin-cypress": "2.0.1", | ||
"eslint-plugin-jest": "21.18.0", | ||
@@ -67,0 +70,0 @@ "eslint-plugin-promise": "3.7.0", |
@@ -9,2 +9,4 @@ # react-d3-graph · [](https://travis-ci.org/danielcaldas/react-d3-graph) | ||
[](https://paypal.me/DanielCaldas321) | ||
:book: [documentation](https://danielcaldas.github.io/react-d3-graph/docs/index.html) | ||
@@ -22,4 +24,5 @@ | ||
* [small dataset](https://goodguydaniel.com/react-d3-graph/sandbox/index.html?data=small) | ||
* [custom node dataset](https://goodguydaniel.com/react-d3-graph/sandbox/index.html?data=custom-node) | ||
* [small dataset](https://goodguydaniel.com/react-d3-graph/sandbox/index.html?data=small) - small example. | ||
* [custom node dataset](https://goodguydaniel.com/react-d3-graph/sandbox/index.html?data=custom-node) - sample config with custom views. | ||
* [marvel dataset!](https://goodguydaniel.com/react-d3-graph/sandbox/index.html?data=marvel) - sample config with directed collapsible graph and custom svg nodes. | ||
@@ -38,3 +41,2 @@ Do you want to visualize your own data set on the live sandbox? Just submit a PR! You're welcome 😁 | ||
npm install react-d3-graph // using npm | ||
yarn add react-d3-graph // using yarn | ||
``` | ||
@@ -71,2 +73,6 @@ | ||
// graph event callbacks | ||
const onClickGraph = function() { | ||
window.alert(`Clicked the graph background`); | ||
}; | ||
const onClickNode = function(nodeId) { | ||
@@ -76,2 +82,6 @@ window.alert(`Clicked node ${nodeId}`); | ||
const onRightClickNode = function(event, nodeId) { | ||
window.alert(`Right clicked node ${nodeId}`); | ||
}; | ||
const onMouseOverNode = function(nodeId) { | ||
@@ -89,2 +99,6 @@ window.alert(`Mouse over node ${nodeId}`); | ||
const onRightClickLink = function(event, source, target) { | ||
window.alert(`Right clicked link between ${source} and ${target}`); | ||
}; | ||
const onMouseOverLink = function(source, target) { | ||
@@ -103,3 +117,6 @@ window.alert(`Mouse over in link between ${source} and ${target}`); | ||
onClickNode={onClickNode} | ||
onRightClickNode={onRightClickNode} | ||
onClickGraph={onClickGraph} | ||
onClickLink={onClickLink} | ||
onRightClickLink={onRightClickLink} | ||
onMouseOverNode={onMouseOverNode} | ||
@@ -121,4 +138,10 @@ onMouseOutNode={onMouseOutNode} | ||
## Donation | ||
Using _react-d3-graph_ and want to help the project grow with new features or simply want to say thank you? You can always buy me a cup of coffee ☕☕☕ | ||
[](https://paypal.me/DanielCaldas321) | ||
## Alternatives (Not what you where looking for?) | ||
Well if you scrolled this far maybe _react-d3-graph_ does not fulfill all your requirements 😭, but don't worry I got you covered! There are a lot of different and good alternatives out there, [here is a list with a few alternatives](http://anvaka.github.io/graph-drawing-libraries/#!/all#%2Fall). Btw, not in the previous list but also a valid alternative built by uber [uber/react-vis-force](https://github.com/uber/react-vis-force). |
@@ -40,6 +40,11 @@ /** | ||
* rearrange all nodes positions based on new position of dragged node (note: **staticGraph** should be false). | ||
* @param {boolean} [collapsible=false] - 🚅🚅🚅 Allow leaf neighbours nodes to be collapsed (folded), this will allow users to clear the way out and focus on the parts of the graph that really matter. | ||
* To see an example of this behavior you can access this sandbox link that has a specific set up to experiment this feature. | ||
* @param {boolean} [collapsible=false] - 🚅🚅🚅 Allow leaf neighbors nodes to be collapsed (folded), this will allow users to clear the way out and focus on the parts of the graph that really matter. | ||
* To see an example of this behavior you can access this sandbox link that has a specific set up to experiment this feature. **NOTE**: At this moment | ||
* nodes without connections (orphan nodes) are not rendered when this property is activated (see [react-d3-graph/issues/#129](https://github.com/danielcaldas/react-d3-graph/issues/129)). | ||
* <br/> | ||
* <img src="https://github.com/danielcaldas/react-d3-graph/blob/master/docs/rd3g-collapsible.gif?raw=true" width="820" height="480"/> | ||
* @param {boolean} [directed=false] - This property makes react-d3-graph handle your graph as a directed graph. It will | ||
* out of the box provide the look and feel of a directed graph and add directional semantic to links. You can see a sample in the image below. | ||
* <br/> | ||
* <img src="https://github.com/danielcaldas/react-d3-graph/blob/master/docs/rd3g-directed.gif?raw=true" width="820" height="480"/> | ||
* @param {number} [height=400] - the height of the (svg) area where the graph will be rendered. | ||
@@ -58,2 +63,18 @@ * @param {boolean} [nodeHighlightBehavior=false] - 🚅🚅🚅 when user mouse hovers a node that node and adjacent common | ||
* @param {number} [minZoom=0.1] - min zoom that can be performed against the graph. | ||
* @param {number} [focusZoom=1] - zoom that will be applied when the graph view is focused in a node. Its value must be between | ||
* *minZoom* and *maxZoom*. If the specified *focusZoom* is out of this range, *minZoom* or *maxZoom* will be applied instead. | ||
* **NOTE:** This animation is not trigger by default. In order to trigger it you need to pass down to `react-d3-graph` the | ||
* node that you want to focus via prop `focusedNodeId` along side with nodes and links: | ||
* | ||
* ```javascript | ||
* const data = { | ||
* nodes: this.state.data.nodes, | ||
* links: this.state.data.links, | ||
* focusedNodeId: 'nodeIdToTriggerZoomAnimation' | ||
* }; | ||
* ``` | ||
* | ||
* <img src="https://github.com/danielcaldas/react-d3-graph/blob/master/docs/rd3g-zoom-animation.gif?raw=true" width="820" height="480"/> | ||
* | ||
* @param {number} [focusAnimationDuration=0.75] - duration (in seconds) for the animation that takes place when focusing the graph on a node. | ||
* @param {boolean} [panAndZoom=false] - 🚅🚅🚅 pan and zoom effect when performing zoom in the graph, | ||
@@ -92,5 +113,15 @@ * a similar functionality may be consulted {@link https://bl.ocks.org/mbostock/2a39a768b1d4bc00a09650edef75ad39|here}. | ||
* @param {number} [node.highlightStrokeWidth=1.5] - strokeWidth in highlighted state. | ||
* @param {string} [node.labelProperty='id'] - this is the node property that will be used in runtime to | ||
* @param {string|Function} [node.labelProperty='id'] - this is the node property that will be used in runtime to | ||
* fetch the label content. You just need to add some property (e.g. firstName) to the node payload and then set | ||
* node.labelProperty to be **'firstName'**. | ||
* node.labelProperty to be **'firstName'**. **This can also be a function!**, if you pass a function here it will be called | ||
* to obtain the `label` value on the fly, as a client you will receive all the node information that you passed down into react-d3-graph, | ||
* so the signature of the function would be: | ||
* ```javascript | ||
* function myCustomLabelBuilder(node) { | ||
* // do stuff to get the final result... | ||
* return 'label string'; | ||
* } | ||
* ``` | ||
* Then you just need to make sure that you pass this function in the config in `config.node.labelProperty`. | ||
* <br/> | ||
* @param {string} [node.mouseCursor='pointer'] - {@link https://developer.mozilla.org/en/docs/Web/CSS/cursor?v=control|cursor} | ||
@@ -102,3 +133,3 @@ * property for when some node is mouse hovered. | ||
* @param {number} [node.size=200] - 🔍🔍🔍 defines the size of all nodes. | ||
* @param {string} [node.strokeColor='none'] - color for the stroke of each node. | ||
* @param {string} [node.strokeColor='none'] - 🔍🔍🔍 this is the stroke color that will be applied to the node if no **strokeColor property** is found inside the node itself (yes **you can pass a property 'strokeColor' inside the node and that stroke color will override this default one** ). | ||
* @param {number} [node.strokeWidth=1.5] - the width of the all node strokes. | ||
@@ -120,3 +151,3 @@ * @param {string} [node.svg=''] - 🔍🔍🔍 render custom svg for nodes in alternative to **node.symbolType**. This svg can | ||
* **[note]** react-d3-graph will map this values to [d3 symbols](https://github.com/d3/d3-shape#symbols) | ||
* @param {Function} [node.viewGenerator=undefined] - 🔍🔍🔍 function that receives a node and returns a JSX view. | ||
* @param {Function} [node.viewGenerator=null] - 🔍🔍🔍 function that receives a node and returns a JSX view. | ||
* <br/> | ||
@@ -127,2 +158,6 @@ * @param {Object} link link object is explained in the next section. ⬇️ | ||
* (from version 1.3.0 this property can be configured at link level). | ||
* @param {string} [link.highlightColor='#d3d3d3'] - links' color in highlight state. | ||
* <img src="https://github.com/danielcaldas/react-d3-graph/blob/master/docs/rd3g-bend.gif?raw=true" width="820" height="480"/> | ||
* @param {string} [link.mouseCursor='pointer'] - {@link https://developer.mozilla.org/en/docs/Web/CSS/cursor?v=control|cursor} | ||
* property for when link is mouse hovered. | ||
* @param {number} [link.opacity=1] - the default opacity value for links. | ||
@@ -140,3 +175,2 @@ * @param {boolean} [link.semanticStrokeWidth=false] - when set to true all links will have | ||
* ``` | ||
* @param {string} [link.highlightColor='#d3d3d3'] - links' color in highlight state. | ||
* @param {string} [link.type='STRAIGHT'] - the type of line to draw, available types at this point are: | ||
@@ -147,3 +181,2 @@ * - "STRAIGHT" <small>(default)</small> - a straight line. | ||
* <br/> | ||
* <img src="https://github.com/danielcaldas/react-d3-graph/blob/master/docs/rd3g-bend.gif?raw=true" width="820" height="480"/> | ||
* | ||
@@ -169,2 +202,3 @@ * @example | ||
collapsible: false, | ||
directed: false, | ||
height: 400, | ||
@@ -176,2 +210,4 @@ highlightDegree: 1, | ||
minZoom: 0.1, | ||
focusZoom: 1, | ||
focusAnimationDuration: 0.75, | ||
nodeHighlightBehavior: false, | ||
@@ -205,3 +241,4 @@ panAndZoom: false, | ||
svg: '', | ||
symbolType: 'circle' | ||
symbolType: 'circle', | ||
viewGenerator: null | ||
}, | ||
@@ -211,2 +248,3 @@ link: { | ||
highlightColor: '#d3d3d3', | ||
mouseCursor: 'pointer', | ||
opacity: 1, | ||
@@ -213,0 +251,0 @@ semanticStrokeWidth: false, |
@@ -1,2 +0,1 @@ | ||
/*eslint-disable max-lines*/ | ||
/** | ||
@@ -36,2 +35,3 @@ * @module Graph/helper | ||
import { buildLinkPathDefinition } from '../link/link.helper'; | ||
import { getMarkerId } from '../marker/marker.helper'; | ||
@@ -98,2 +98,3 @@ const NODE_PROPS_WHITELIST = ['id', 'highlighted', 'x', 'y', 'index', 'vy', 'vx']; | ||
* @param {Array.<Link>} graphLinks - an array of all graph links. | ||
* @param {Object} config - the graph config. | ||
* @returns {Object.<string, Object>} an object containing a matrix of connections of the graph, for each nodeId, | ||
@@ -103,3 +104,3 @@ * there is an object that maps adjacent nodes ids (string) and their values (number). | ||
*/ | ||
function _initializeLinks(graphLinks) { | ||
function _initializeLinks(graphLinks, config) { | ||
return graphLinks.reduce((links, l) => { | ||
@@ -117,5 +118,10 @@ const source = l.source.id || l.source; | ||
// TODO: If the graph is directed this should be adapted | ||
links[source][target] = links[target][source] = l.value || 1; | ||
const value = config.collapsible && l.isHidden ? 0 : l.value || 1; | ||
links[source][target] = value; | ||
if (!config.directed) { | ||
links[target][source] = value; | ||
} | ||
return links; | ||
@@ -163,10 +169,20 @@ }, {}); | ||
* @param {Array.<Object>} d3Links - all d3Links. | ||
* @param {Object} 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 d3Link. | ||
* @memberof Graph/helper | ||
*/ | ||
function _mapDataLinkToD3Link(link, index, d3Links = []) { | ||
function _mapDataLinkToD3Link(link, index, d3Links = [], config, state = {}) { | ||
const d3Link = d3Links[index]; | ||
if (d3Link) { | ||
return d3Link; | ||
const toggledDirected = state.config && state.config.directed && config.directed !== state.config.directed; | ||
// every time we toggle directed config all links should be visible again | ||
if (toggledDirected) { | ||
return { ...d3Link, isHidden: false }; | ||
} | ||
// every time we disable collapsible (collapsible is false) all links should be visible again | ||
return config.collapsible ? d3Link : { ...d3Link, isHidden: false }; | ||
} | ||
@@ -282,15 +298,16 @@ | ||
const markerId = config.directed ? getMarkerId(highlight, transform, config) : null; | ||
return { | ||
markerId, | ||
d, | ||
source, | ||
target, | ||
x1, | ||
y1, | ||
x2, | ||
y2, | ||
strokeWidth, | ||
stroke, | ||
mouseCursor: config.link.mouseCursor, | ||
className: CONST.LINK_CLASS_NAME, | ||
opacity, | ||
onClickLink: linkCallbacks.onClickLink, | ||
onRightClickLink: linkCallbacks.onRightClickLink, | ||
onMouseOverLink: linkCallbacks.onMouseOverLink, | ||
@@ -324,3 +341,3 @@ onMouseOutLink: linkCallbacks.onMouseOutLink | ||
let stroke = config.node.strokeColor; | ||
let stroke = node.strokeColor || config.node.strokeColor; | ||
@@ -331,2 +348,8 @@ if (highlight && config.node.highlightStrokeColor !== CONST.KEYWORDS.SAME) { | ||
let label = node[config.node.labelProperty] || node.id; | ||
if (typeof config.node.labelProperty === 'function') { | ||
label = config.node.labelProperty(node); | ||
} | ||
const t = 1 / transform; | ||
@@ -352,4 +375,5 @@ const nodeSize = node.size || config.node.size; | ||
id: node.id, | ||
label: node[config.node.labelProperty] || node.id, | ||
label, | ||
onClickNode: nodeCallbacks.onClickNode, | ||
onRightClickNode: nodeCallbacks.onRightClickNode, | ||
onMouseOverNode: nodeCallbacks.onMouseOverNode, | ||
@@ -418,3 +442,3 @@ onMouseOut: nodeCallbacks.onMouseOut, | ||
const configUpdated = | ||
newConfig && !utils.isObjectEmpty(newConfig) && !utils.isDeepEqual(newConfig, currentState.config); | ||
newConfig && !utils.isEmptyObject(newConfig) && !utils.isDeepEqual(newConfig, currentState.config); | ||
const d3ConfigUpdated = newConfig && newConfig.d3 && !utils.isDeepEqual(newConfig.d3, currentState.config.d3); | ||
@@ -448,3 +472,3 @@ | ||
), | ||
links: data.links.map((l, index) => _mapDataLinkToD3Link(l, index, state && state.d3Links)) | ||
links: data.links.map((l, index) => _mapDataLinkToD3Link(l, index, state && state.d3Links, config, state)) | ||
}; | ||
@@ -460,3 +484,3 @@ } else { | ||
let nodes = _initializeNodes(graph.nodes); | ||
let links = _initializeLinks(graph.links); // matrix of graph connections | ||
let links = _initializeLinks(graph.links, newConfig); // matrix of graph connections | ||
const { nodes: d3Nodes, links: d3Links } = graph; | ||
@@ -466,2 +490,10 @@ const formatedId = id.replace(/ /g, '_'); | ||
const { minZoom, maxZoom, focusZoom } = newConfig; | ||
if (focusZoom > maxZoom) { | ||
newConfig.focusZoom = maxZoom; | ||
} else if (focusZoom < minZoom) { | ||
newConfig.focusZoom = minZoom; | ||
} | ||
return { | ||
@@ -514,98 +546,23 @@ id: formatedId, | ||
/** | ||
* This function disconnects all the connections from leaf -> parent. | ||
* @param {string} targetNodeId - The id of the node from which to disconnect the leaf nodes | ||
* @param {Object.<string, number>} originalConnections - An object containing a matrix of connections of the nodes. | ||
* @param {Array} d3Links - An array containing all the d3 links. | ||
* @returns {Object.<string, number>} - Contains the new links and d3Links. | ||
* Returns the transformation to apply in order to center the graph on the | ||
* selected node. | ||
* @param {Object} d3Node - node to focus the graph view on. | ||
* @param {Object} config - same as {@link #buildGraph|config in buildGraph}. | ||
* @returns {string} transform rule to apply. | ||
* @memberof Graph/helper | ||
*/ | ||
function disconnectLeafNodeConnections(targetNodeId, originalConnections, d3Links) { | ||
const leafNodesToToggle = getLeafNodeConnections(targetNodeId, originalConnections); | ||
const toggledLeafNodes = Object.keys(leafNodesToToggle).reduce((newLeafNodeConnections, leafNodeId) => { | ||
// toggle connections from Leaf node to Parent node | ||
newLeafNodeConnections[leafNodeId] = toggleNodeConnection(targetNodeId, originalConnections[leafNodeId]); | ||
function getCenterAndZoomTransformation(d3Node, config) { | ||
if (!d3Node) { | ||
return; | ||
} | ||
return newLeafNodeConnections; | ||
}, {}); | ||
const { width, height, focusZoom } = config; | ||
const toggledLeafNodesList = Object.keys(toggledLeafNodes); | ||
const toggledD3Links = d3Links.reduce((allD3Links, currentD3Link) => { | ||
const { source, target } = currentD3Link; | ||
const isLeafNode = toggledLeafNodesList.some( | ||
leafNodeId => leafNodeId === `${source.id}` || leafNodeId === `${target.id}` | ||
); | ||
isLeafNode | ||
? allD3Links.push({ ...currentD3Link, isHidden: !currentD3Link.isHidden }) | ||
: allD3Links.push(currentD3Link); | ||
return allD3Links; | ||
}, []); | ||
return { | ||
d3Links: toggledD3Links, | ||
links: { | ||
...originalConnections, | ||
...toggledLeafNodes | ||
} | ||
}; | ||
return ` | ||
translate(${width / 2}, ${height / 2}) | ||
scale(${focusZoom}) | ||
translate(${-d3Node.x}, ${-d3Node.y}) | ||
`; | ||
} | ||
/** | ||
* This function toggles the value for a given node connection (1 -> 0 and vice-versa). | ||
* @param {string} targetNodeId - The id of the node which to toggle | ||
* @param {Object.<string, number>} allConnections - An object containing a matrix of connections of the node | ||
* where we want to toggle the connection (destinations/targets). | ||
* @returns {Object.<string, number>} - Contains the new connections with the target node toggled. | ||
* @memberof Graph/helper | ||
*/ | ||
function toggleNodeConnection(targetNodeId, allConnections) { | ||
const newConnection = { | ||
[targetNodeId]: allConnections[targetNodeId] === 1 ? 0 : 1 | ||
}; | ||
return { ...allConnections, ...newConnection }; | ||
} | ||
/** | ||
* Based on a starting node (ID) and all the current connections between all the nodes. | ||
* Find the leaf node connections of that starting node. | ||
* @param {string} startingNodeId - The id of the node where the "search" should be started. | ||
* @param {Object.<string, number>} currentConnections - An object containing a matrix of connections of the nodes. | ||
* @returns {Object.<string, number>} - Contains the connections to leaf nodes based on the given starting node. | ||
* @memberof Graph/helper | ||
*/ | ||
function getLeafNodeConnections(startingNodeId, currentConnections) { | ||
const startingNodeConnections = currentConnections[startingNodeId]; | ||
const startingNodeConnectionsList = Object.keys(startingNodeConnections); | ||
return startingNodeConnectionsList.reduce((allLeafNodes, candidateLeafId) => { | ||
const candidateLeafConnections = currentConnections[candidateLeafId]; | ||
const candidateLeafConnectionList = Object.keys(candidateLeafConnections); | ||
const isLeafNode = candidateLeafConnectionList.length === 1; | ||
if (isLeafNode) { | ||
allLeafNodes[candidateLeafId] = candidateLeafConnections; | ||
} | ||
return allLeafNodes; | ||
}, {}); | ||
} | ||
/** | ||
* Given a node and the connections matrix, give the cardinality of the node. | ||
* | ||
* i.e.: Taking into account the node is connected to nothing, it amounts to 0. | ||
* Being connected to three nodes, it amounts to 3. | ||
* @param {string} nodeId - The id of the node to get the cardinality of | ||
* @param {Object.<string, number>} linksMatrix - An object containing a matrix of connections of the nodes. | ||
* @returns {number} - Contains the cardinality of the asked node. | ||
* @memberof Graph/helper | ||
*/ | ||
function getNodeCardinality(nodeId, linksMatrix) { | ||
const nodeConnectivityList = Object.values(linksMatrix[nodeId] || []); | ||
return nodeConnectivityList.reduce((cardinality, nodeConnectivity) => cardinality + nodeConnectivity, 0); | ||
} | ||
export { | ||
@@ -616,8 +573,5 @@ buildLinkProps, | ||
checkForGraphElementsChanges, | ||
disconnectLeafNodeConnections, | ||
getLeafNodeConnections, | ||
getNodeCardinality, | ||
initializeGraphState, | ||
toggleNodeConnection, | ||
updateNodeHighlightedValue | ||
updateNodeHighlightedValue, | ||
getCenterAndZoomTransformation | ||
}; |
@@ -12,4 +12,5 @@ import React from 'react'; | ||
import * as collapseHelper from './collapse.helper'; | ||
import * as graphHelper from './graph.helper'; | ||
import * as graphRenderer from './graph.renderer'; | ||
import * as graphHelper from './graph.helper'; | ||
import utils from '../../utils'; | ||
@@ -53,2 +54,6 @@ | ||
* // graph event callbacks | ||
* const onClickGraph = function() { | ||
* window.alert('Clicked the graph background'); | ||
* }; | ||
* | ||
* const onClickNode = function(nodeId) { | ||
@@ -58,2 +63,6 @@ * window.alert('Clicked node ${nodeId}'); | ||
* | ||
* const onRightClickNode = function(event, nodeId) { | ||
* window.alert('Right clicked node ${nodeId}'); | ||
* }; | ||
* | ||
* const onMouseOverNode = function(nodeId) { | ||
@@ -71,2 +80,6 @@ * window.alert(`Mouse over node ${nodeId}`); | ||
* | ||
* const onRightClickLink = function(event, source, target) { | ||
* window.alert('Right clicked link between ${source} and ${target}'); | ||
* }; | ||
* | ||
* const onMouseOverLink = function(source, target) { | ||
@@ -84,4 +97,7 @@ * window.alert(`Mouse over in link between ${source} and ${target}`); | ||
* config={myConfig} | ||
* onClickGraph={onClickGraph} | ||
* onClickNode={onClickNode} | ||
* onRightClickNode={onRightClickNode} | ||
* onClickLink={onClickLink} | ||
* onRightClickLink={onRightClickLink} | ||
* onMouseOverNode={onMouseOverNode} | ||
@@ -157,3 +173,8 @@ * onMouseOutNode={onMouseOutNode} | ||
*/ | ||
_onDragStart = () => this.pauseSimulation(); | ||
_onDragStart = () => { | ||
this.pauseSimulation(); | ||
if (this.state.enableFocusAnimation) { | ||
this.setState({ enableFocusAnimation: false }); | ||
} | ||
}; | ||
@@ -175,5 +196,6 @@ /** | ||
* @param {Object} state - new state to pass on. | ||
* @param {Function} [cb] - optional callback to fed in to {@link setState()|https://reactjs.org/docs/react-component.html#setstate}. | ||
* @returns {undefined} | ||
*/ | ||
_tick = (state = {}) => this.setState(state); | ||
_tick = (state = {}, cb) => (cb ? this.setState(state, cb) : this.setState(state)); | ||
@@ -302,2 +324,3 @@ /** | ||
this.focusAnimationTimeout = null; | ||
this.state = graphHelper.initializeGraphState(this.props, this.state); | ||
@@ -331,2 +354,7 @@ } | ||
const focusedNodeId = nextProps.data.focusedNodeId; | ||
const d3FocusedNode = this.state.d3Nodes.find(node => `${node.id}` === `${focusedNodeId}`); | ||
const focusTransformation = graphHelper.getCenterAndZoomTransformation(d3FocusedNode, this.state.config); | ||
const enableFocusAnimation = this.props.data.focusedNodeId !== nextProps.data.focusedNodeId; | ||
this.setState({ | ||
@@ -338,3 +366,6 @@ ...state, | ||
newGraphElements, | ||
transform | ||
transform, | ||
focusedNodeId, | ||
enableFocusAnimation, | ||
focusTransformation | ||
}); | ||
@@ -379,19 +410,82 @@ } | ||
if (this.state.config.collapsible) { | ||
const disconnectedLeafNodesPartialState = graphHelper.disconnectLeafNodeConnections( | ||
const leafConnections = collapseHelper.getTargetLeafConnections( | ||
clickedNodeId, | ||
this.state.links, | ||
this.state.d3Links | ||
this.state.config | ||
); | ||
const links = collapseHelper.toggleLinksMatrixConnections( | ||
this.state.links, | ||
leafConnections, | ||
this.state.config | ||
); | ||
const d3Links = collapseHelper.toggleLinksConnections(this.state.d3Links, links); | ||
this._tick({ ...disconnectedLeafNodesPartialState }); | ||
this._tick( | ||
{ | ||
links, | ||
d3Links | ||
}, | ||
() => this.props.onClickNode && this.props.onClickNode(clickedNodeId) | ||
); | ||
} else { | ||
this.props.onClickNode && this.props.onClickNode(clickedNodeId); | ||
} | ||
}; | ||
this.props.onClickNode && this.props.onClickNode(clickedNodeId); | ||
/** | ||
* Calls the callback passed to the component. | ||
* @param {Object} e - The event of onClick handler. | ||
* @returns {undefined} | ||
*/ | ||
onClickGraph = e => { | ||
if (this.state.enableFocusAnimation) { | ||
this.setState({ enableFocusAnimation: false }); | ||
} | ||
// Only trigger the graph onClickHandler, if not clicked a node or link. | ||
// toUpperCase() is added as a precaution, as the documentation says tagName should always | ||
// return in UPPERCASE, but chrome returns lowercase | ||
if ( | ||
e.target.tagName.toUpperCase() === 'SVG' && | ||
e.target.attributes.name.value === `svg-container-${this.state.id}` | ||
) { | ||
this.props.onClickGraph && this.props.onClickGraph(); | ||
} | ||
}; | ||
/** | ||
* Obtain a set of properties which will be used to perform the focus and zoom animation if | ||
* required. In case there's not a focus and zoom animation in progress, it should reset the | ||
* transition duration to zero and clear transformation styles. | ||
* @returns {Object} - Focus and zoom animation properties. | ||
*/ | ||
_generateFocusAnimationProps = () => { | ||
const { focusedNodeId } = this.state; | ||
// In case an older animation was still not complete, clear previous timeout to ensure the new one is not cancelled | ||
if (this.state.enableFocusAnimation) { | ||
if (this.focusAnimationTimeout) { | ||
clearTimeout(this.focusAnimationTimeout); | ||
} | ||
this.focusAnimationTimeout = setTimeout( | ||
() => this.setState({ enableFocusAnimation: false }), | ||
this.state.config.focusAnimationDuration * 1000 | ||
); | ||
} | ||
const transitionDuration = this.state.enableFocusAnimation ? this.state.config.focusAnimationDuration : 0; | ||
return { | ||
style: { transitionDuration: `${transitionDuration}s` }, | ||
transform: focusedNodeId ? this.state.focusTransformation : null | ||
}; | ||
}; | ||
render() { | ||
const { nodes, links } = graphRenderer.buildGraph( | ||
const { nodes, links, defs } = graphRenderer.buildGraph( | ||
this.state.nodes, | ||
{ | ||
onClickNode: this.onClickNode, | ||
onRightClickNode: this.props.onRightClickNode, | ||
onMouseOverNode: this.onMouseOverNode, | ||
@@ -404,2 +498,3 @@ onMouseOut: this.onMouseOutNode | ||
onClickLink: this.props.onClickLink, | ||
onRightClickLink: this.props.onRightClickLink, | ||
onMouseOverLink: this.onMouseOverLink, | ||
@@ -419,6 +514,9 @@ onMouseOutLink: this.onMouseOutLink | ||
const containerProps = this._generateFocusAnimationProps(); | ||
return ( | ||
<div id={`${this.state.id}-${CONST.GRAPH_WRAPPER_ID}`}> | ||
<svg style={svgStyle}> | ||
<g id={`${this.state.id}-${CONST.GRAPH_CONTAINER_ID}`}> | ||
<svg name={`svg-container-${this.state.id}`} style={svgStyle} onClick={this.onClickGraph}> | ||
{defs} | ||
<g id={`${this.state.id}-${CONST.GRAPH_CONTAINER_ID}`} {...containerProps}> | ||
{links} | ||
@@ -425,0 +523,0 @@ {nodes} |
@@ -9,6 +9,9 @@ /** | ||
import CONST from './graph.const'; | ||
import { MARKERS, MARKER_SMALL_SIZE, MARKER_MEDIUM_OFFSET, MARKER_LARGE_OFFSET } from '../marker/marker.const'; | ||
import Link from '../link/Link'; | ||
import Node from '../node/Node'; | ||
import { buildLinkProps, buildNodeProps, getNodeCardinality } from './graph.helper'; | ||
import Marker from '../marker/Marker'; | ||
import { buildLinkProps, buildNodeProps } from './graph.helper'; | ||
import { isNodeVisible } from './collapse.helper'; | ||
@@ -25,7 +28,13 @@ /** | ||
* @param {number} transform - value that indicates the amount of zoom transformation. | ||
* @returns {Object[]} returns the generated array of Link components. | ||
* @memberof Graph/renderer | ||
* @returns {Array.<Object>} returns the generated array of Link components. | ||
* @memberof Graph/helper | ||
*/ | ||
function _buildLinks(nodes, links, linksMatrix, config, linkCallbacks, highlightedNode, highlightedLink, transform) { | ||
return links.filter(({ isHidden }) => !isHidden).map(link => { | ||
let outLinks = links; | ||
if (config.collapsible) { | ||
outLinks = outLinks.filter(({ isHidden }) => !isHidden); | ||
} | ||
return outLinks.map(link => { | ||
const { source, target } = link; | ||
@@ -62,4 +71,4 @@ // FIXME: solve this source data inconsistency later | ||
* @param {Object.<string, Object>} linksMatrix - the matrix of connections of the graph | ||
* @returns {Object} returns the generated array of nodes components | ||
* @memberof Graph/renderer | ||
* @returns {Array.<Object>} returns the generated array of node components | ||
* @memberof Graph/helper | ||
*/ | ||
@@ -70,3 +79,3 @@ function _buildNodes(nodes, nodeCallbacks, config, highlightedNode, highlightedLink, transform, linksMatrix) { | ||
if (config.collapsible) { | ||
outNodes = outNodes.filter(nodeId => getNodeCardinality(nodeId, linksMatrix) > 0); | ||
outNodes = outNodes.filter(nodeId => isNodeVisible(nodeId, linksMatrix)); | ||
} | ||
@@ -89,2 +98,44 @@ | ||
/** | ||
* Builds graph defs (for now markers, but we could also have gradients for instance). | ||
* NOTE: defs are static svg graphical objects, thus we only need to render them once, the result | ||
* is cached on the 1st call and from there we simply return the cached jsx. | ||
* @returns {Function} memoized build definitions function. | ||
* @memberof Graph/helper | ||
*/ | ||
function _buildDefs() { | ||
let cachedDefs; | ||
return config => { | ||
if (cachedDefs) { | ||
return cachedDefs; | ||
} | ||
const small = MARKER_SMALL_SIZE; | ||
const medium = small + MARKER_MEDIUM_OFFSET * config.maxZoom / 3; | ||
const large = small + MARKER_LARGE_OFFSET * config.maxZoom / 3; | ||
cachedDefs = ( | ||
<defs> | ||
<Marker id={MARKERS.MARKER_S} refX={small} fill={config.link.color} /> | ||
<Marker id={MARKERS.MARKER_SH} refX={small} fill={config.link.highlightColor} /> | ||
<Marker id={MARKERS.MARKER_M} refX={medium} fill={config.link.color} /> | ||
<Marker id={MARKERS.MARKER_MH} refX={medium} fill={config.link.highlightColor} /> | ||
<Marker id={MARKERS.MARKER_L} refX={large} fill={config.link.color} /> | ||
<Marker id={MARKERS.MARKER_LH} refX={large} fill={config.link.highlightColor} /> | ||
</defs> | ||
); | ||
return cachedDefs; | ||
}; | ||
} | ||
/** | ||
* Memoized reference for _buildDefs. | ||
* @param {Object} config - an object containing rd3g consumer defined configurations {@link #config config} for the graph. | ||
* @returns {Object} graph reusable objects [defs](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs). | ||
* @memberof Graph/helper | ||
*/ | ||
const _memoizedBuildDefs = _buildDefs(); | ||
/** | ||
* Method that actually is exported an consumed by Graph component in order to build all Nodes and Link | ||
@@ -152,3 +203,4 @@ * components. | ||
transform | ||
) | ||
), | ||
defs: _memoizedBuildDefs(config) | ||
}; | ||
@@ -155,0 +207,0 @@ } |
@@ -51,3 +51,3 @@ /** | ||
* *CURVE_SMOOTH* type inspired by {@link http://bl.ocks.org/mbostock/1153292|mbostock - Mobile Patent Suits}. | ||
* @param {string} type type of curve to get radius strategy from. | ||
* @param {string} [type=LINE_TYPES.STRAIGHT] type of curve to get radius strategy from. | ||
* @returns {Function} a function that calculates a radius | ||
@@ -58,3 +58,3 @@ * to match curve type expectation. Fallback is the straight line. | ||
function getRadiusStrategy(type) { | ||
return RADIUS_STRATEGIES[type]; | ||
return RADIUS_STRATEGIES[type] || RADIUS_STRATEGIES[LINE_TYPES.STRAIGHT]; | ||
} | ||
@@ -61,0 +61,0 @@ |
@@ -10,2 +10,6 @@ import React from 'react'; | ||
* | ||
* const onRightClickLink = function(source, target) { | ||
* window.alert(`Right clicked link between ${source} and ${target}`); | ||
* }; | ||
* | ||
* const onMouseOverLink = function(source, target) { | ||
@@ -23,6 +27,3 @@ * window.alert(`Mouse over in link between ${source} and ${target}`); | ||
* target='idTargetNode' | ||
* x1=22 | ||
* y1=22 | ||
* x2=22 | ||
* y2=22 | ||
* markerId='marker-small' | ||
* strokeWidth=1.5 | ||
@@ -32,3 +33,5 @@ * stroke='green' | ||
* opacity=1 | ||
* mouseCursor='pointer' | ||
* onClickLink={onClickLink} | ||
* onRightClickLink={onRightClickLink} | ||
* onMouseOverLink={onMouseOverLink} | ||
@@ -45,2 +48,10 @@ * onMouseOutLink={onMouseOutLink} /> | ||
/** | ||
* Handle link right click event. | ||
* @param {Object} event - native event. | ||
* @returns {undefined} | ||
*/ | ||
handleOnRightClickLink = event => | ||
this.props.onRightClickLink && this.props.onRightClickLink(event, this.props.source, this.props.target); | ||
/** | ||
* Handle mouse over link event. | ||
@@ -64,3 +75,4 @@ * @returns {undefined} | ||
opacity: this.props.opacity, | ||
fill: 'none' | ||
fill: 'none', | ||
cursor: this.props.mouseCursor | ||
}; | ||
@@ -72,13 +84,14 @@ | ||
onClick: this.handleOnClickLink, | ||
onContextMenu: this.handleOnRightClickLink, | ||
onMouseOut: this.handleOnMouseOutLink, | ||
onMouseOver: this.handleOnMouseOverLink, | ||
style: lineStyle, | ||
x1: this.props.x1, | ||
x2: this.props.x2, | ||
y1: this.props.y1, | ||
y2: this.props.y2 | ||
style: lineStyle | ||
}; | ||
if (this.props.markerId) { | ||
lineProps.markerEnd = `url(#${this.props.markerId})`; | ||
} | ||
return <path {...lineProps} />; | ||
} | ||
} |
@@ -14,2 +14,6 @@ import React from 'react'; | ||
* | ||
* const onRightClickNode = function(nodeId) { | ||
* window.alert('Right clicked node', nodeId); | ||
* } | ||
* | ||
* const onMouseOverNode = function(nodeId) { | ||
@@ -43,2 +47,3 @@ * window.alert('Mouse over node', nodeId); | ||
* onClickNode={onClickNode} | ||
* onRightClickNode={onRightClickNode} | ||
* onMouseOverNode={onMouseOverNode} | ||
@@ -55,2 +60,9 @@ * onMouseOutNode={onMouseOutNode} /> | ||
/** | ||
* Handle right click on the node. | ||
* @param {Object} event - native event. | ||
* @returns {undefined} | ||
*/ | ||
handleOnRightClickNode = event => this.props.onRightClickNode && this.props.onRightClickNode(event, this.props.id); | ||
/** | ||
* Handle mouse over node event. | ||
@@ -71,2 +83,3 @@ * @returns {undefined} | ||
onClick: this.handleOnClickNode, | ||
onContextMenu: this.handleOnRightClickNode, | ||
onMouseOut: this.handleOnMouseOutNode, | ||
@@ -73,0 +86,0 @@ onMouseOver: this.handleOnMouseOverNode, |
@@ -20,3 +20,3 @@ /** | ||
function _isPropertyNestedObject(o, k) { | ||
return o.hasOwnProperty(k) && typeof o[k] === 'object' && o[k] !== null && !isObjectEmpty(o[k]); | ||
return o.hasOwnProperty(k) && typeof o[k] === 'object' && o[k] !== null && !isEmptyObject(o[k]); | ||
} | ||
@@ -39,3 +39,3 @@ | ||
if ((isObjectEmpty(o1) && !isObjectEmpty(o2)) || (!isObjectEmpty(o1) && isObjectEmpty(o2))) { | ||
if ((isEmptyObject(o1) && !isEmptyObject(o2)) || (!isEmptyObject(o1) && isEmptyObject(o2))) { | ||
return false; | ||
@@ -57,3 +57,3 @@ } | ||
} else { | ||
const r = (isObjectEmpty(o1[k]) && isObjectEmpty(o2[k])) || (o2.hasOwnProperty(k) && o2[k] === o1[k]); | ||
const r = (isEmptyObject(o1[k]) && isEmptyObject(o2[k])) || (o2.hasOwnProperty(k) && o2[k] === o1[k]); | ||
@@ -78,3 +78,3 @@ diffs.push(r); | ||
*/ | ||
function isObjectEmpty(o) { | ||
function isEmptyObject(o) { | ||
return !!o && typeof o === 'object' && !Object.keys(o).length; | ||
@@ -97,3 +97,3 @@ } | ||
if (Object.keys(o1 || {}).length === 0) { | ||
return o2 && !isObjectEmpty(o2) ? o2 : {}; | ||
return o2 && !isEmptyObject(o2) ? o2 : {}; | ||
} | ||
@@ -163,3 +163,3 @@ | ||
isDeepEqual, | ||
isObjectEmpty, | ||
isEmptyObject, | ||
merge, | ||
@@ -166,0 +166,0 @@ pick, |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
391562
50
5258
140
45