Skip to content

Commit b99192d

Browse files
committed
Implements nodeSvgShape prop (#31)
Add docs for nodeSvgShape prop
1 parent 2b14a4f commit b99192d

File tree

4 files changed

+106
-29
lines changed

4 files changed

+106
-29
lines changed

README.md

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ React D3 Tree is a [React](http://facebook.github.io/react/) component that lets
1313
- [Installation](#installation)
1414
- [Usage](#usage)
1515
- [Props](#props)
16+
- [Node shapes](#node-shapes)
1617
- [Styling](#styling)
1718
- [External data sources](#external-data-sources)
1819

@@ -78,24 +79,55 @@ class MyComponent extends React.Component {
7879

7980

8081
## Props
81-
| Property | Type | Options | Required? | Default | Description |
82-
|:---------------------|:----------------|:--------------------------------------|:----------|:--------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
83-
| `data` | `array` | | required | `undefined` | Single-element array containing hierarchical object (see `myTreeData` above). <br /> Contains (at least) `name` and `parent` keys. |
84-
| `onClick` | `func` | | | `undefined` | Callback function to be called whenever a node is clicked. <br /><br /> The clicked node's data object is passed to the callback function as the first parameter. |
85-
| `orientation` | `string` (enum) | `horizontal` `vertical` | | `horizontal` | `horizontal` - Tree expands left-to-right. <br /><br /> `vertical` - Tree expands top-to-bottom. |
86-
| `translate` | `object` | | | `{x: 0, y: 0}` | Translates the graph along the x/y axis by the specified amount of pixels (avoids the graph being stuck in the top left canvas corner). |
87-
| `pathFunc` | `string` (enum) | `diagonal` `elbow` `straight` | | `diagonal` | `diagonal` - Renders smooth, curved edges between parent-child nodes. <br /><br /> `elbow` - Renders sharp edges at right angles between parent-child nodes. <br /><br /> `straight` - Renders straight lines between parent-child nodes. |
88-
| `collapsible` | `bool` | | | `true` | Toggles ability to collapse/expand the tree's nodes by clicking them. |
89-
| `initialDepth` | `number` | `0..n` | | `undefined` | Sets the maximum node depth to which the tree is expanded on its initial render. <br /> Tree renders to full depth if prop is omitted. |
90-
| `depthFactor` | `number` | `-n..0..n` | | `undefined` | Ensures the tree takes up a fixed amount of space (`node.y = node.depth * depthFactor`), regardless of tree depth. <br /> **TIP**: Negative values invert the tree's direction. |
91-
| `zoomable` | `bool` | | | `true` | Toggles ability to zoom in/out on the Tree by scaling it according to `props.scaleExtent`. |
92-
| `scaleExtent` | `object` | `{min: 0..n, max: 0..n}` | | `{min: 0.1, max: 1}` | Sets the minimum/maximum extent to which the tree can be scaled if `props.zoomable` is true. |
93-
| `nodeSize` | `object` | `{x: 0..n, y: 0..n}` | | `{x: 140, y: 140}` | Sets a fixed size for each node. <br /><br /> This does not affect node circle sizes, circle sizes are handled by the `circleRadius` prop. |
94-
| `separation` | `object` | `{siblings: 0..n, nonSiblings: 0..n}` | | `{siblings: 1, nonSiblings: 2}` | Sets separation between neighbouring nodes, differentiating between siblings (same parent) and non-siblings. |
95-
| `circleRadius` | `number` | `0..n` | | `10` | Sets the radius of each node's `<circle>` element. |
96-
| `transitionDuration` | `number` | `0..n` | | `500` | Sets the animation duration (in ms) of each expansion/collapse of a tree node. <br /><br /> Set this to `0` to deactivate animations completely. |
97-
| `styles` | `object` | see [Styling](#styling) | | `Node`/`Link` CSS files | Overrides and/or enhances the tree's default styling. |
82+
| Property | Type | Options | Required? | Default | Description |
83+
|:------------------------|:----------------|:--------------------------------------|:----------|:---------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
84+
| `data` | `array` | | required | `undefined` | Single-element array containing hierarchical object (see `myTreeData` above). <br /> Contains (at least) `name` and `parent` keys. |
85+
| `nodeSvgShape` | `object` | see [Node shapes](#node-shapes) | | `{shape: 'circle', shapeProps: r: 10}` | Sets a specific SVG shape element + shapeProps to be used for each node. |
86+
| `onClick` | `func` | | | `undefined` | Callback function to be called whenever a node is clicked. <br /><br /> The clicked node's data object is passed to the callback function as the first parameter. |
87+
| `orientation` | `string` (enum) | `horizontal` `vertical` | | `horizontal` | `horizontal` - Tree expands left-to-right. <br /><br /> `vertical` - Tree expands top-to-bottom. |
88+
| `translate` | `object` | | | `{x: 0, y: 0}` | Translates the graph along the x/y axis by the specified amount of pixels (avoids the graph being stuck in the top left canvas corner). |
89+
| `pathFunc` | `string` (enum) | `diagonal` `elbow` `straight` | | `diagonal` | `diagonal` - Renders smooth, curved edges between parent-child nodes. <br /><br /> `elbow` - Renders sharp edges at right angles between parent-child nodes. <br /><br /> `straight` - Renders straight lines between parent-child nodes. |
90+
| `collapsible` | `bool` | | | `true` | Toggles ability to collapse/expand the tree's nodes by clicking them. |
91+
| `initialDepth` | `number` | `0..n` | | `undefined` | Sets the maximum node depth to which the tree is expanded on its initial render. <br /> Tree renders to full depth if prop is omitted. |
92+
| `depthFactor` | `number` | `-n..0..n` | | `undefined` | Ensures the tree takes up a fixed amount of space (`node.y = node.depth * depthFactor`), regardless of tree depth. <br /> **TIP**: Negative values invert the tree's direction. |
93+
| `zoomable` | `bool` | | | `true` | Toggles ability to zoom in/out on the Tree by scaling it according to `props.scaleExtent`. |
94+
| `scaleExtent` | `object` | `{min: 0..n, max: 0..n}` | | `{min: 0.1, max: 1}` | Sets the minimum/maximum extent to which the tree can be scaled if `props.zoomable` is true. |
95+
| `nodeSize` | `object` | `{x: 0..n, y: 0..n}` | | `{x: 140, y: 140}` | Sets a fixed size for each node. <br /><br /> This does not affect node circle sizes, circle sizes are handled by the `circleRadius` prop. |
96+
| `separation` | `object` | `{siblings: 0..n, nonSiblings: 0..n}` | | `{siblings: 1, nonSiblings: 2}` | Sets separation between neighbouring nodes, differentiating between siblings (same parent) and non-siblings. |
97+
| `transitionDuration` | `number` | `0..n` | | `500` | Sets the animation duration (in ms) of each expansion/collapse of a tree node. <br /><br /> Set this to `0` to deactivate animations completely. |
98+
| `styles` | `object` | see [Styling](#styling) | | `Node`/`Link` CSS files | Overrides and/or enhances the tree's default styling. |
99+
| `circleRadius` (legacy) | `number` | `0..n` | | `undefined` | Sets the radius of each node's `<circle>` element.<br /><br /> **Will be deprecated in v2, please use `nodeSvgShape` instead.** |
100+
101+
102+
## Node shapes
103+
The `nodeSvgShape` prop allows specifying any [SVG shape primitive](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Basic_Shapes) to describe how the tree's nodes should be shaped.
104+
105+
> Note: `nodeSvgShape` and `circleRadius` are mutually exclusive props. `nodeSvgShape` will be used unless the legacy `circleRadius` is specified.
106+
107+
For example, assuming we want to use squares instead of the default circles, we can do:
108+
```js
109+
const svgSquare = {
110+
shape: 'rect',
111+
shapeProps: {
112+
width: 20,
113+
height: 20,
114+
x: -10,
115+
y: -10,
116+
}
117+
}
118+
119+
// ...
120+
121+
<Tree data={myTreeData} nodeSvgShape={svgSquare}>
122+
```
123+
124+
### Overridable `shapeProps`
125+
`shapeProps` is currently merged with `node.circle`/`leafNode.circle` (see [Styling](#styling)).
126+
127+
This means any properties passed in `shapeProps` will be overridden by **properties with the same key** in the `node.circle`/`leafNode.circle` style props.
128+
This is to prevent breaking the legacy usage of `circleRadius` + styling via `node/leafNode` properties until it is deprecated fully in v2.
98129

130+
**From v1.5.x onwards, it is therefore recommended to pass all node styling properties through `shapeProps`**.
99131

100132
## Styling
101133
The tree's `styles` prop may be used to override any of the tree's default styling.

src/Node/index.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export default class Node extends React.Component {
6868
}
6969

7070
render() {
71-
const { nodeData, styles } = this.props;
71+
const { nodeData, nodeSvgShape, styles } = this.props;
7272
const nodeStyle = nodeData._children
7373
? { ...styles.node }
7474
: { ...styles.leafNode };
@@ -83,6 +83,16 @@ export default class Node extends React.Component {
8383
transform={this.state.transform}
8484
onClick={this.handleClick}
8585
>
86+
{/* TODO: DEPRECATE <circle /> */}
87+
{this.props.circleRadius ? (
88+
<circle r={this.props.circleRadius} style={nodeStyle.circle} />
89+
) : (
90+
React.createElement(nodeSvgShape.shape, {
91+
...nodeSvgShape.shapeProps,
92+
...nodeStyle.circle,
93+
})
94+
)}
95+
8696
<text
8797
className="nodeNameBase"
8898
textAnchor={this.props.textAnchor}
@@ -93,9 +103,6 @@ export default class Node extends React.Component {
93103
>
94104
{this.props.name}
95105
</text>
96-
97-
<circle r={this.props.circleRadius} style={nodeStyle.circle} />
98-
99106
<text
100107
className="nodeAttributesBase"
101108
y="0"
@@ -117,6 +124,7 @@ export default class Node extends React.Component {
117124
Node.defaultProps = {
118125
textAnchor: 'start',
119126
attributes: undefined,
127+
circleRadius: undefined,
120128
styles: {
121129
node: {
122130
circle: {},
@@ -133,12 +141,13 @@ Node.defaultProps = {
133141

134142
Node.propTypes = {
135143
nodeData: PropTypes.object.isRequired,
144+
nodeSvgShape: PropTypes.object.isRequired,
136145
orientation: PropTypes.oneOf(['horizontal', 'vertical']).isRequired,
137146
transitionDuration: PropTypes.number.isRequired,
138147
onClick: PropTypes.func.isRequired,
139148
name: PropTypes.string.isRequired,
140149
attributes: PropTypes.object,
141150
textAnchor: PropTypes.string,
142-
circleRadius: PropTypes.number.isRequired,
151+
circleRadius: PropTypes.number,
143152
styles: PropTypes.object,
144153
};

src/Node/tests/index.test.js

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@ describe('<Node />', () => {
1818

1919
const mockProps = {
2020
nodeData,
21+
nodeSvgShape: {
22+
shape: 'circle',
23+
shapeProps: {
24+
r: 10,
25+
},
26+
},
2127
name: nodeData.name,
2228
orientation: 'horizontal',
2329
transitionDuration: 500,
2430
onClick: () => {},
25-
circleRadius: 10,
2631
styles: {},
2732
};
2833

@@ -65,11 +70,11 @@ describe('<Node />', () => {
6570
/>,
6671
);
6772

68-
expect(leafNodeComponent.find('circle').prop('style')).toBe(
69-
fixture.leafNode.circle,
73+
expect(leafNodeComponent.find('circle').prop('fill')).toBe(
74+
fixture.leafNode.circle.fill,
7075
);
71-
expect(nodeComponent.find('circle').prop('style')).toBe(
72-
fixture.node.circle,
76+
expect(nodeComponent.find('circle').prop('fill')).toBe(
77+
fixture.node.circle.fill,
7378
);
7479
});
7580

@@ -213,7 +218,26 @@ describe('<Node />', () => {
213218
);
214219
});
215220

216-
// TODO Find a way to meaningfully test `componentWillLeave`
221+
it('allows passing SVG shape elements + shapeProps to be used as the node element', () => {
222+
const fixture = { shape: 'ellipsis', shapeProps: { rx: 20, ry: 10 } };
223+
const props = { ...mockProps, nodeSvgShape: fixture };
224+
const renderedComponent = shallow(<Node {...props} />);
225+
226+
expect(renderedComponent.find(fixture.shape).length).toBe(1);
227+
expect(renderedComponent.find(fixture.shape).props()).toEqual(
228+
fixture.shapeProps,
229+
);
230+
});
231+
232+
// TODO: DEPRECATE in v2
233+
it('falls back to legacy `<circle />` prop only if `circleRadius` is defined', () => {
234+
const props = { ...mockProps, circleRadius: 99 };
235+
const renderedComponent = shallow(<Node {...props} />);
236+
237+
expect(renderedComponent.find('circle').prop('r')).toBe(99);
238+
});
239+
240+
// TODO: Find a way to meaningfully test `componentWillLeave`
217241

218242
// it('regresses to its parent coords when unmounting/leaving', () => {
219243
// jest.spyOn(Node.prototype, 'applyTransform');

src/Tree/index.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ export default class Tree extends React.Component {
261261
render() {
262262
const { nodes, links } = this.generateTree();
263263
const {
264+
nodeSvgShape,
264265
orientation,
265266
translate,
266267
pathFunc,
@@ -296,6 +297,7 @@ export default class Tree extends React.Component {
296297
{nodes.map(nodeData => (
297298
<Node
298299
key={nodeData.id}
300+
nodeSvgShape={nodeSvgShape}
299301
orientation={orientation}
300302
transitionDuration={transitionDuration}
301303
textAnchor="start"
@@ -315,6 +317,12 @@ export default class Tree extends React.Component {
315317
}
316318

317319
Tree.defaultProps = {
320+
nodeSvgShape: {
321+
shape: 'circle',
322+
shapeProps: {
323+
r: 10,
324+
},
325+
},
318326
onClick: undefined,
319327
orientation: 'horizontal',
320328
translate: { x: 0, y: 0 },
@@ -327,12 +335,16 @@ Tree.defaultProps = {
327335
scaleExtent: { min: 0.1, max: 1 },
328336
nodeSize: { x: 140, y: 140 },
329337
separation: { siblings: 1, nonSiblings: 2 },
330-
circleRadius: 10,
338+
circleRadius: undefined, // TODO: DEPRECATE
331339
styles: {},
332340
};
333341

334342
Tree.propTypes = {
335343
data: PropTypes.array.isRequired,
344+
nodeSvgShape: PropTypes.shape({
345+
shape: PropTypes.string,
346+
shapeProps: PropTypes.object,
347+
}),
336348
onClick: PropTypes.func,
337349
orientation: PropTypes.oneOf(['horizontal', 'vertical']),
338350
translate: PropTypes.shape({

0 commit comments

Comments
 (0)