Skip to content

Commit 0374b09

Browse files
LonelyPrincessdanielcaldas
authored andcommitted
Focus view on a node (#107)
* feat: Focus graph on a node It is now possible to set a "focusedNodeId" to the graph, which will result in the selected node appearing on the center of the displayed area of the graph. Optionally, aside from centering the view on it, a certain zoom can be applied while the node is focused on screen. No zoom is applied by default, but that behaviour can be configured by using the new "focusZoom" parameter in the config object. This setting should receive a number with the zoom level to apply, so the larger it is, the bigger the node will appear on screen. * feat: Node focus feature example added to sandbox * fix: Ensure that focusZoom is between minZoom and maxZoom * fix: Avoid issue with id datatypes when searching for focused node * refactor: Improve readability for focusZoom bounds Nested ternary expressions replaced by a more readable if-else expression when checking if the `focusZoom` property set by the user is between the configured `minZoom` and `maxZoom`. * refactor: Remove statements in library code * revert: Removed blank lines in DOCUMENTATION.md * docs: Added more information on focusZoom property * feat: Customize focus animation length * refactor: focusNodeId wrapped in data props Instead of being a prop on its own, the 'focusedNodeId' must now be included as part of the 'data' object received by the Graph component. The sandbox code has been update to reflect this change. * refactor: focus transformation function moved to graphHelper The 'getCenterAndZoomTransformation' has been rellocated to the graph helper file, instead of being part of the Graph component. * docs: Updated jsdoc for getCenterAndZoomTransformation * refactor: Renamed config param for focus animation duration * test: Graph snapshot updated * fix: Consider hidden links when obtaining matrix The link matrix was not been properly updated when the collapsible setting was enabled. This is because when obtaining its values it didn't check whether the different links were hidden or not. This change fixes the issue and prevents the link matrix from containing wrong values which cause orphan nodes are not hidden properly during collapse. * fix: Reset transition duration after focus animation The transition duration property was been applied to the graph container at all times, which affected to every other transformations that triggered on it. This included the default zoom or the drag and drop functionality, among others. In order to fix this, that transition duration value is set back to zero after the focus and zoom animation has finished. Some actions on the node do also cancel the animation in case it's still ongoing, like trying to drag a node. With this change, there's no longer a delay when moving a node to another location or while performing a zoom. * test: Graph snapshot updated with default transition duration * fix: Drag & drop in focused node There was an issue with the drag and drop feature when used along the focus and zoom transformation. The problem was that we were recalculating the transformation everytime the page was rendered, which meant that a new transform value would be created anytime the node moved to a new position. This issue only affected the focused node itself, since the `getCenterAndZoomTransformation` did only depend on that node coordinates. To avoid abnormal behaviours, the transformation is only obtained when the graph receives props. By doing this, the current view position is preserved until a new focused node is set. If the user drags the node around, current zoom and position will be unaffected by it. This matches the default behaviour we encounter in any other scenario, when moving any other nodes or not having activated the focus and zoom feature. * refactor: Reset transition duration on drag start The `enableFocusAnimation` property in Graph' state is now set to `false` in the `dragStart` event instead of during `dragMove`. This change should have a positive impact in the application's perfomance, as the `enableFocusAnimation` value will only checked once instead of being read in every position change during a drag operation. * style: Added blank line after variable declaration * chore: Browser environment added to ESLint config * refactor: getCenterAndZoomTransformation receives d3Node object * fix: Node not centered when focusing with zoom One of the latest changes broke the focus feature in certain scenarios. When a focus zoom different from one was applied, the view was not properly centered in the selected node. The transformation obtained in the 'getCenterAndZoomTransformation' did not work as expected. Applying a single translate before scaling didn't seem to do the trick, and the second translate is necessary for the view to display the selected node in the center after scaling the graph. * refactor: Store focusedNodeId in Graph component state
1 parent 661c587 commit 0374b09

File tree

8 files changed

+114
-8
lines changed

8 files changed

+114
-8
lines changed

.eslintrc.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ module.exports = {
1616
}
1717
},
1818
plugins: ['standard', 'promise', 'react', 'jest', 'cypress'],
19+
env: {
20+
browser: true
21+
},
1922
rules: {
2023
'react/jsx-uses-react': 'error',
2124
'react/jsx-uses-vars': 'error',

docs/DOCUMENTATION.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -877,9 +877,11 @@ components.
877877
},
878878
...
879879
}
880+
880881
```
881882
882883
```
884+
883885
* `linkCallbacks` **[Array][87]<[Function][83]>** array of callbacks for used defined event handler for link interactions.
884886
* `config` **[Object][74]** an object containing rd3g consumer defined configurations [config][92] for the graph.
885887
* `highlightedNode` **[string][78]** this value contains a string that represents the some currently highlighted node.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
"start": "http-server ./sandbox/ -p 8888 -c-1",
2626
"test:clean": "jest --no-cache --updateSnapshot --verbose --coverage --config jest.config.js",
2727
"test:watch": "jest --verbose --coverage --watchAll --config jest.config.js",
28-
"test": "jest --verbose --coverage --config jest.config.js"
28+
"test": "jest --verbose --coverage --config jest.config.js",
29+
"sandbox": "npm run dist:sandbox && npm run start"
2930
},
3031
"lint-staged": {
3132
"*.{js,jsx,json,css,md}": [

sandbox/Sandbox.jsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,15 @@ export default class Sandbox extends React.Component {
5353

5454
onClickGraph = () => console.info(`Clicked the graph`);
5555

56-
onClickNode = id => !this.state.config.collapsible && window.alert(`Clicked node ${id}`);
56+
onClickNode = id => {
57+
!this.state.config.collapsible && window.alert(`Clicked node ${id}`);
58+
this.setState({
59+
data: {
60+
...this.state.data,
61+
focusedNodeId: this.state.data.focusedNodeId !== id ? id : null
62+
}
63+
});
64+
};
5765

5866
onRightClickNode = (event, id) => {
5967
event.preventDefault();
@@ -299,7 +307,8 @@ export default class Sandbox extends React.Component {
299307
// to true in the constructor we will provide nodes with initial positions
300308
const data = {
301309
nodes: this.decorateGraphNodesWithInitialPositioning(this.state.data.nodes),
302-
links: this.state.data.links
310+
links: this.state.data.links,
311+
focusedNodeId: this.state.data.focusedNodeId
303312
};
304313

305314
const graphProps = {

src/components/graph/Graph.jsx

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,12 @@ export default class Graph extends React.Component {
165165
* Handles d3 drag 'start' event.
166166
* @returns {undefined}
167167
*/
168-
_onDragStart = () => this.pauseSimulation();
168+
_onDragStart = () => {
169+
this.pauseSimulation();
170+
if (this.state.enableFocusAnimation) {
171+
this.setState({ enableFocusAnimation: false });
172+
}
173+
};
169174

170175
/**
171176
* Sets nodes and links highlighted value.
@@ -309,6 +314,7 @@ export default class Graph extends React.Component {
309314
utils.throwErr(this.constructor.name, ERRORS.GRAPH_NO_ID_PROP);
310315
}
311316

317+
this.focusAnimationTimeout = null;
312318
this.state = graphHelper.initializeGraphState(this.props, this.state);
313319
}
314320

@@ -337,13 +343,21 @@ export default class Graph extends React.Component {
337343

338344
const transform = newConfig.panAndZoom !== this.state.config.panAndZoom ? 1 : this.state.transform;
339345

346+
const focusedNodeId = nextProps.data.focusedNodeId;
347+
const d3FocusedNode = this.state.d3Nodes.find(node => `${node.id}` === `${focusedNodeId}`);
348+
const focusTransformation = graphHelper.getCenterAndZoomTransformation(d3FocusedNode, this.state.config);
349+
const enableFocusAnimation = this.props.data.focusedNodeId !== nextProps.data.focusedNodeId;
350+
340351
this.setState({
341352
...state,
342353
config,
343354
configUpdated,
344355
d3ConfigUpdated,
345356
newGraphElements,
346-
transform
357+
transform,
358+
focusedNodeId,
359+
enableFocusAnimation,
360+
focusTransformation
347361
});
348362
}
349363

@@ -413,6 +427,10 @@ export default class Graph extends React.Component {
413427
* @returns {undefined}
414428
*/
415429
onClickGraph = e => {
430+
if (this.state.enableFocusAnimation) {
431+
this.setState({ enableFocusAnimation: false });
432+
}
433+
416434
// Only trigger the graph onClickHandler, if not clicked a node or link.
417435
// toUpperCase() is added as a precaution, as the documentation says tagName should always
418436
// return in UPPERCASE, but chrome returns lowercase
@@ -424,6 +442,35 @@ export default class Graph extends React.Component {
424442
}
425443
};
426444

445+
/**
446+
* Obtain a set of properties which will be used to perform the focus and zoom animation if
447+
* required. In case there's not a focus and zoom animation in progress, it should reset the
448+
* transition duration to zero and clear transformation styles.
449+
* @returns {Object} - Focus and zoom animation properties.
450+
*/
451+
_generateFocusAnimationProps = () => {
452+
const { focusedNodeId } = this.state;
453+
454+
// In case an older animation was still not complete, clear previous timeout to ensure the new one is not cancelled
455+
if (this.state.enableFocusAnimation) {
456+
if (this.focusAnimationTimeout) {
457+
clearTimeout(this.focusAnimationTimeout);
458+
}
459+
460+
this.focusAnimationTimeout = setTimeout(
461+
() => this.setState({ enableFocusAnimation: false }),
462+
this.state.config.focusAnimationDuration * 1000
463+
);
464+
}
465+
466+
const transitionDuration = this.state.enableFocusAnimation ? this.state.config.focusAnimationDuration : 0;
467+
468+
return {
469+
style: { transitionDuration: `${transitionDuration}s` },
470+
transform: focusedNodeId ? this.state.focusTransformation : null
471+
};
472+
};
473+
427474
render() {
428475
const { nodes, links, defs } = graphRenderer.buildGraph(
429476
this.state.nodes,
@@ -452,11 +499,13 @@ export default class Graph extends React.Component {
452499
width: this.state.config.width
453500
};
454501

502+
const containerProps = this._generateFocusAnimationProps();
503+
455504
return (
456505
<div id={`${this.state.id}-${CONST.GRAPH_WRAPPER_ID}`}>
457506
<svg name={`svg-container-${this.state.id}`} style={svgStyle} onClick={this.onClickGraph}>
458507
{defs}
459-
<g id={`${this.state.id}-${CONST.GRAPH_CONTAINER_ID}`}>
508+
<g id={`${this.state.id}-${CONST.GRAPH_CONTAINER_ID}`} {...containerProps}>
460509
{links}
461510
{nodes}
462511
</g>

src/components/graph/graph.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@
5959
* the value the more the less highlighted nodes will be visible (related to *nodeHighlightBehavior*).
6060
* @param {number} [maxZoom=8] - max zoom that can be performed against the graph.
6161
* @param {number} [minZoom=0.1] - min zoom that can be performed against the graph.
62+
* @param {number} [focusZoom=1] - zoom that will be applied when the graph view is focused in a node. Its value must be between
63+
* *minZoom* and *maxZoom*. If the specified *focusZoom* is out of this range, *minZoom* or *maxZoom* will be applied instead.
64+
* @param {number} [focusAnimationDuration=0.75] - duration (in seconds) for the animation that takes place when focusing the graph on a node.
6265
* @param {boolean} [panAndZoom=false] - 🚅🚅🚅 pan and zoom effect when performing zoom in the graph,
6366
* a similar functionality may be consulted {@link https://bl.ocks.org/mbostock/2a39a768b1d4bc00a09650edef75ad39|here}.
6467
* @param {boolean} [staticGraph=false] - when setting this value to true the graph will be completely static, thus
@@ -182,6 +185,8 @@ export default {
182185
linkHighlightBehavior: false,
183186
maxZoom: 8,
184187
minZoom: 0.1,
188+
focusZoom: 1,
189+
focusAnimationDuration: 0.75,
185190
nodeHighlightBehavior: false,
186191
panAndZoom: false,
187192
staticGraph: false,

src/components/graph/graph.helper.js

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ function _initializeLinks(graphLinks, config) {
112112
links[target] = {};
113113
}
114114

115-
const value = l.value || 1;
115+
const value = config.collapsible && l.isHidden ? 0 : l.value || 1;
116116

117117
links[source][target] = value;
118118

@@ -475,6 +475,14 @@ function initializeGraphState({ data, id, config }, state) {
475475
const formatedId = id.replace(/ /g, '_');
476476
const simulation = _createForceSimulation(newConfig.width, newConfig.height, newConfig.d3 && newConfig.d3.gravity);
477477

478+
const { minZoom, maxZoom, focusZoom } = newConfig;
479+
480+
if (focusZoom > maxZoom) {
481+
newConfig.focusZoom = maxZoom;
482+
} else if (focusZoom < minZoom) {
483+
newConfig.focusZoom = minZoom;
484+
}
485+
478486
return {
479487
id: formatedId,
480488
config: newConfig,
@@ -521,11 +529,34 @@ function updateNodeHighlightedValue(nodes, links, config, id, value = false) {
521529
};
522530
}
523531

532+
/**
533+
* Returns the transformation to apply in order to center the graph on the
534+
* selected node.
535+
* @param {Object} d3Node - node to focus the graph view on.
536+
* @param {Object} config - same as {@link #buildGraph|config in buildGraph}.
537+
* @returns {string} transform rule to apply.
538+
* @memberof Graph/helper
539+
*/
540+
function getCenterAndZoomTransformation(d3Node, config) {
541+
if (!d3Node) {
542+
return;
543+
}
544+
545+
const { width, height, focusZoom } = config;
546+
547+
return `
548+
translate(${width / 2}, ${height / 2})
549+
scale(${focusZoom})
550+
translate(${-d3Node.x}, ${-d3Node.y})
551+
`;
552+
}
553+
524554
export {
525555
buildLinkProps,
526556
buildNodeProps,
527557
checkForGraphConfigChanges,
528558
checkForGraphElementsChanges,
529559
initializeGraphState,
530-
updateNodeHighlightedValue
560+
updateNodeHighlightedValue,
561+
getCenterAndZoomTransformation
531562
};

test/snapshot/graph/__snapshots__/graph.snapshot.test.js.snap

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ exports[`Snapshot - Graph Component should match snapshot 1`] = `
108108
</defs>
109109
<g
110110
id="graphId-graph-container-zoomable"
111+
style={
112+
Object {
113+
"transitionDuration": "0s",
114+
}
115+
}
116+
transform={null}
111117
>
112118
<path
113119
className="link"

0 commit comments

Comments
 (0)