Skip to content

Commit b66e6e4

Browse files
LonelyPrincessdanielcaldas
authored andcommitted
Customize nodes with JSX views (#103)
* feat: Customize nodes with JSX views A new 'view' property can now be passed down to graph nodes. This new property will receive a view, which can include plain HTML or even React components. Please, note that this is only available as a low level property in individual nodes and cannot be defined in the config object. This feature is mostly thought for dynamic content, so 'svg' property should be enough to cover any scenario in which the nodes' content don't change depending on their inner data. Also, if the node has values for both 'svg' and 'view' properties, 'view' will always prevail. If we want 'svg' to be applied instead, we must not assign a value to 'view'. * fix: Wrapper for node's custom view Inside of the 'foreignObject' tag, there is now a body tag which will wrap around the defined custom view. This wrapper has the same dimensions that were set as node size in the config object. Because this wrapper has explicit dimensions now, the developer will be able to use relative sizes (such as percentages) in their custom view's parent element. This fixes an issue where the custom component's height and width were set to 100%, and it failed to retrieve the size for the 'foreignObject' tag. Having a real parent in the embedded HTML solves the problem, as our new view is no longer unable to retrieve its dimensions. * feat: Custom view sample added to sandbox The sandbox includes now a new example where the custom view property is demonstrated. In this case, each node represents a person and, for every one of them, an additional set of data is included. The defined custom view displays all of this data: name, gender and whether they're able to drive a bike and / or a car. The values for the 'customView' property are generated dynamically in the 'custom-node.data.js' file. In order for the user to watch this example, they must enter the sandbox with the following query param: {default sandbox url}?data=custom-node * fix: Error unmounting nodes with custom view in sandbox example When trying to remove nodes in the sandbox example for the custom view feature, the application threw the following error. ``` <body> tried to unmount. Because of cross-browser quirks it is impossible to unmount some top-level components (eg <html>, <head>, and <body>) reliably and efficiently. To fix this, have a single top-level component that never unmounts render these elements. ``` This was introduced when I added a body tag as a wrapper for the embedded code that serves as a custom view. Replacing that tag with a lower-level one fixed the issue. * feat: View generator function to customize nodes The `view` property where we could assign an already generated view to our nodes has been replaced by a new property `viewGenerator`. This new property can be defined both at the global configuration object or at node level, and it will consists on a function that receives a node object and uses its data to return a view: ``` viewGenerator: (node) => <CustomViewComponent node={node} ``` With the previous method, the node view was not automatically updated whenever the node data changed. The only way for this to happen was by manually overriding the value of the the `view` property. With the current approach, there are a couple of advantages: * The view generator can be defined in the config object, so we don't have to set a property for the view in every node. All of the nodes will share this global setting. * The library will be in charge of rendering the node custom view whenever the data changes, as the generator method receives the current node data as a parameter and returns a view generated using this values. This means the developer doesn't have to worry about updating a view property on each node everytime their data changes, as the view will be automatically updated anytime the Node component `render` method is called. The sandbox example for this feature has also been updated to reflect the new changes. * docs: Fixed datatype for viewGenerator in config docs * refactor: Inner property to override view generator When building a node component props, a new property called `overrideViewGenerator` is added. This property is set to `true` when there's a svg provided for an specific node which doesn't have its own view generator. In this case, we assume the svg should have precedence over the default view generator set in the global settings. In any other scenario, the view generator will always be applied instead of the svg. Only when `overrideViewGenerator` is `true` will we assume the `svg` property to be more important. Before, this was done by setting the `viewGenerator` property to `null`, but the new property makes the code more readable. * refactor: Pass all node data to viewGenerator Instead of passing only the node information entered by the user via the `data` prop of the `Graph` component, the `viewGenerator` function will now receive everything, including auto-generated data such as its current coordinates in the graph or any values inherited from the default config object. * refactor: overrideViewGenerator property renamed to overrideGlobalViewGenerator * test: buildNodeProps tests updated with new attributes The `buildNodeProps` related tests were failing due to the new properties added in this PR. This included both `viewGenerator`, `overrideGlobalViewGenerator`, as well as other properties inherited from the `that.node` object. * test refactor: Implicit values inherited from node Since some of the recently added values are directly inherited from the `that.node` data object and `buildNodeProps` does not alter them in any way, they are no longer hardcoded in the test `toEqual` check. The object `that.node` contents are added via the spread operator instead, which will make things easier if the sample data changes at some point.
1 parent 1fc9733 commit b66e6e4

File tree

12 files changed

+512
-6
lines changed

12 files changed

+512
-6
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from 'react';
2+
3+
import './res/styles/custom-node.css';
4+
5+
const ICON_PATH = './data/custom-node/res/images/';
6+
7+
const ICON_TYPES = {
8+
MAN: ICON_PATH + 'man.svg',
9+
WOMAN: ICON_PATH + 'girl.svg',
10+
CAR: ICON_PATH + 'car.svg',
11+
BIKE: ICON_PATH + 'bike.svg'
12+
};
13+
14+
/**
15+
* Component that renders a person's name and gender, along with icons
16+
* representing if they have a driver license for bike and / or car.
17+
*/
18+
function CustomNode({ person }) {
19+
const isMale = person.gender === 'male';
20+
21+
return (
22+
<div className={`flex-container person-node ${isMale ? 'male' : 'female'}`}>
23+
<div className="name">{person.name}</div>
24+
25+
<div className="flex-container fill-space flex-container-row">
26+
<div className="fill-space">
27+
<div
28+
className="icon"
29+
style={{ backgroundImage: `url('${isMale ? ICON_TYPES.MAN : ICON_TYPES.WOMAN}')` }}
30+
/>
31+
</div>
32+
33+
<div className="icon-bar">
34+
{person.hasBike && (
35+
<div className="icon" style={{ backgroundImage: `url('${ICON_TYPES.BIKE}')` }} />
36+
)}
37+
{person.hasCar && <div className="icon" style={{ backgroundImage: `url('${ICON_TYPES.CAR}')` }} />}
38+
</div>
39+
</div>
40+
</div>
41+
);
42+
}
43+
44+
export default CustomNode;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from 'react';
2+
import CustomNode from './CustomNode';
3+
4+
module.exports = {
5+
automaticRearrangeAfterDropNode: false,
6+
collapsible: false,
7+
height: 400,
8+
highlightDegree: 1,
9+
highlightOpacity: 0.2,
10+
linkHighlightBehavior: true,
11+
maxZoom: 8,
12+
minZoom: 0.1,
13+
nodeHighlightBehavior: true,
14+
panAndZoom: false,
15+
staticGraph: false,
16+
width: 800,
17+
node: {
18+
color: '#d3d3d3',
19+
fontColor: 'black',
20+
fontSize: 12,
21+
fontWeight: 'normal',
22+
highlightColor: 'red',
23+
highlightFontSize: 12,
24+
highlightFontWeight: 'bold',
25+
highlightStrokeColor: 'SAME',
26+
highlightStrokeWidth: 1.5,
27+
labelProperty: 'name',
28+
mouseCursor: 'pointer',
29+
opacity: 1,
30+
renderLabel: false,
31+
size: 700,
32+
strokeColor: 'none',
33+
strokeWidth: 1.5,
34+
svg: '',
35+
symbolType: 'circle',
36+
viewGenerator: node => <CustomNode person={node} />
37+
},
38+
link: {
39+
color: '#d3d3d3',
40+
opacity: 1,
41+
semanticStrokeWidth: false,
42+
strokeWidth: 4,
43+
highlightColor: 'blue'
44+
}
45+
};
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
module.exports = {
2+
links: [
3+
{
4+
source: 1,
5+
target: 2
6+
},
7+
{
8+
source: 1,
9+
target: 3
10+
},
11+
{
12+
source: 1,
13+
target: 4
14+
},
15+
{
16+
source: 3,
17+
target: 4
18+
}
19+
],
20+
nodes: [
21+
{
22+
id: 1,
23+
name: 'Mary',
24+
gender: 'female',
25+
hasCar: false,
26+
hasBike: false
27+
},
28+
{
29+
id: 2,
30+
name: 'Roy',
31+
gender: 'male',
32+
hasCar: false,
33+
hasBike: true
34+
},
35+
{
36+
id: 3,
37+
name: 'Frank',
38+
gender: 'male',
39+
hasCar: true,
40+
hasBike: true
41+
},
42+
{
43+
id: 4,
44+
name: 'Melanie',
45+
gender: 'female',
46+
hasCar: true,
47+
hasBike: false
48+
}
49+
]
50+
};
Lines changed: 58 additions & 0 deletions
Loading
Lines changed: 45 additions & 0 deletions
Loading
Lines changed: 85 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)