Visx is a library that lets us add graphics to our React app easily.
In this article, we’ll look at how to use it to add tree views into our React app.
Install Required Packages
We have to install a few modules.
To get started, we run:
npm i @visx/gradient @visx/group @visx/hierarchy @visx/responsive @visx/shape
to install the packages.
Create the Tree View
We can create tree views with different link types, animations, and layouts by writing:
import React, { useState } from "react";
import { Group } from "@visx/group";
import { hierarchy, Tree } from "@visx/hierarchy";
import { LinearGradient } from "@visx/gradient";
import { pointRadial } from "d3-shape";
import {
LinkHorizontal,
LinkVertical,
LinkRadial,
LinkHorizontalStep,
LinkVerticalStep,
LinkRadialStep,
LinkHorizontalCurve,
LinkVerticalCurve,
LinkRadialCurve,
LinkHorizontalLine,
LinkVerticalLine,
LinkRadialLine
} from "@visx/shape";
const data = {
name: "T",
children: [
{
name: "A",
children: [
{ name: "A1" },
{ name: "A2" },
{
name: "C",
children: [
{
name: "C1"
},
{
name: "D",
children: [
{
name: "D1"
},
{
name: "D2"
},
{
name: "D3"
}
]
}
]
}
]
},
{ name: "Z" },
{
name: "B",
children: [{ name: "B1" }, { name: "B2" }]
}
]
};
function useForceUpdate() {
const [, setValue] = useState(0);
return () => setValue((value) => value + 1);
}
const defaultMargin = { top: 30, left: 30, right: 30, bottom: 70 };
function LinkControls({
layout,
orientation,
linkType,
stepPercent,
setLayout,
setOrientation,
setLinkType,
setStepPercent
}) {
return (
<div>
<label>layout:</label>
<select
onClick={(e) => e.stopPropagation()}
onChange={(e) => setLayout(e.target.value)}
value={layout}
>
<option value="cartesian">cartesian</option>
<option value="polar">polar</option>
</select>
<label>orientation:</label>
<select
onClick={(e) => e.stopPropagation()}
onChange={(e) => setOrientation(e.target.value)}
value={orientation}
disabled={layout === "polar"}
>
<option value="vertical">vertical</option>
<option value="horizontal">horizontal</option>
</select>
<label>link:</label>
<select
onClick={(e) => e.stopPropagation()}
onChange={(e) => setLinkType(e.target.value)}
value={linkType}
>
<option value="diagonal">diagonal</option>
<option value="step">step</option>
<option value="curve">curve</option>
<option value="line">line</option>
</select>
{linkType === "step" && layout !== "polar" && (
<>
<label>step:</label>
<input
onClick={(e) => e.stopPropagation()}
type="range"
min={0}
max={1}
step={0.1}
onChange={(e) => setStepPercent(Number(e.target.value))}
value={stepPercent}
disabled={linkType !== "step" || layout === "polar"}
/>
</>
)}
</div>
);
}
function getLinkComponent({ layout, linkType, orientation }) {
let LinkComponent;
if (layout === "polar") {
if (linkType === "step") {
LinkComponent = LinkRadialStep;
} else if (linkType === "curve") {
LinkComponent = LinkRadialCurve;
} else if (linkType === "line") {
LinkComponent = LinkRadialLine;
} else {
LinkComponent = LinkRadial;
}
} else if (orientation === "vertical") {
if (linkType === "step") {
LinkComponent = LinkVerticalStep;
} else if (linkType === "curve") {
LinkComponent = LinkVerticalCurve;
} else if (linkType === "line") {
LinkComponent = LinkVerticalLine;
} else {
LinkComponent = LinkVertical;
}
} else if (linkType === "step") {
LinkComponent = LinkHorizontalStep;
} else if (linkType === "curve") {
LinkComponent = LinkHorizontalCurve;
} else if (linkType === "line") {
LinkComponent = LinkHorizontalLine;
} else {
LinkComponent = LinkHorizontal;
}
return LinkComponent;
}
function Example({
width: totalWidth,
height: totalHeight,
margin = defaultMargin
}) {
const [layout, setLayout] = useState("cartesian");
const [orientation, setOrientation] = useState("horizontal");
const [linkType, setLinkType] = useState("diagonal");
const [stepPercent, setStepPercent] = useState(0.5);
const forceUpdate = useForceUpdate();
const innerWidth = totalWidth - margin.left - margin.right;
const innerHeight = totalHeight - margin.top - margin.bottom;
let origin;
let sizeWidth;
let sizeHeight;
if (layout === "polar") {
origin = {
x: innerWidth / 2,
y: innerHeight / 2
};
sizeWidth = 2 * Math.PI;
sizeHeight = Math.min(innerWidth, innerHeight) / 2;
} else {
origin = { x: 0, y: 0 };
if (orientation === "vertical") {
sizeWidth = innerWidth;
sizeHeight = innerHeight;
} else {
sizeWidth = innerHeight;
sizeHeight = innerWidth;
}
}
const LinkComponent = getLinkComponent({ layout, linkType, orientation });
return totalWidth < 10 ? null : (
<div>
<LinkControls
layout={layout}
orientation={orientation}
linkType={linkType}
stepPercent={stepPercent}
setLayout={setLayout}
setOrientation={setOrientation}
setLinkType={setLinkType}
setStepPercent={setStepPercent}
/>
<svg width={totalWidth} height={totalHeight}>
<LinearGradient id="links-gradient" from="#fd9b93" to="#fe6e9e" />
<rect width={totalWidth} height={totalHeight} rx={14} fill="#272b4d" />
<Group top={margin.top} left={margin.left}>
<Tree
root={hierarchy(data, (d) => (d.isExpanded ? null : d.children))}
size={[sizeWidth, sizeHeight]}
separation={(a, b) => (a.parent === b.parent ? 1 : 0.5) / a.depth}
>
{(tree) => (
<Group top={origin.y} left={origin.x}>
{tree.links().map((link, i) => (
<LinkComponent
key={i}
data={link}
percent={stepPercent}
stroke="rgb(254,110,158,0.6)"
strokeWidth="1"
fill="none"
/>
))}
{tree.descendants().map((node, key) => {
const width = 40;
const height = 20;
let top;
let left;
if (layout === "polar") {
const [radialX, radialY] = pointRadial(node.x, node.y);
top = radialY;
left = radialX;
} else if (orientation === "vertical") {
top = node.y;
left = node.x;
} else {
top = node.x;
left = node.y;
}
return (
<Group top={top} left={left} key={key}>
{node.depth === 0 && (
<circle
r={12}
fill="url('#links-gradient')"
onClick={() => {
node.data.isExpanded = !node.data.isExpanded;
console.log(node);
forceUpdate();
}}
/>
)}
{node.depth !== 0 && (
<rect
height={height}
width={width}
y={-height / 2}
x={-width / 2}
fill="#272b4d"
stroke={node.data.children ? "#03c0dc" : "#26deb0"}
strokeWidth={1}
strokeDasharray={node.data.children ? "0" : "2,2"}
strokeOpacity={node.data.children ? 1 : 0.6}
rx={node.data.children ? 0 : 10}
onClick={() => {
node.data.isExpanded = !node.data.isExpanded;
forceUpdate();
}}
/>
)}
<text
dy=".33em"
fontSize={9}
fontFamily="Arial"
textAnchor="middle"
style={{ pointerEvents: "none" }}
fill={
node.depth === 0
? "#71248e"
: node.children
? "white"
: "#26deb0"
}
>
{node.data.name}
</text>
</Group>
);
})}
</Group>
)}
</Tree>
</Group>
</svg>
</div>
);
}
export default function App() {
return (
<div className="App">
<Example width={500} height={300} />
</div>
);
}
We have the data
object which we use to render the tree.
The useForceUpdate
hook is used to return the function to force the tree to update when we change the layout, orientation, or link type.
Then we have the LinkControls
component with the dropdowns to let us set the layout, orientation, or link type.
Next, we have the getLinkComponent
function to return the component we render given the layoyt
, linkType
or orientation
.
In the Example
component, we get the layout
, orientation
, and linkType
set in the LinkControls
component.
The Group
component contains all parts of the tree.
Then we add the tree view with the Tree
component.
It has a render prop to with the tree
parameter to get the links between parent and children with the tree.links()
method, we render the links with the LinkComponent
function component.
tree.descendants()
returns the descendants.
We then render the nodes within the tree.descendants().map()
callback.
We set the position of the nodes with the if
statement.
Then we render circle
s or rect
s for the node depending on the depth level of the node.
In either case, we render the text
inside the node, which is either inside the circle
or rect
.
Conclusion
We can render a tree view with cartesian or polar coordinates in our React app with the Visx library.
2 replies on “Add Tree Views into Our React App with the Visx Library”
Hi John,
This was a great example and working on implenting it myself with a few tweaks. I had a question and wondered if you might know more. I am usinig redux and passing in “data” as a prop. Everything works.
render() {
return(
)
}
Within the “onClick” handlers I noticed that any changes to a “node” is also reflecting in my redux store.
“node.data.isExpanded = !node.data.isExpanded;”
“node.data.aNewAttriibute = ‘anyValue”
Do you know if this behavior is expected? It seems like it shouldn’t happen this way as I planned to dispatch an action to update deeply nested nodes in my store.
Thanks!
Hi Matt,
Thanks for reading.
I don’t think there’s any code in the example that commits data to the Redux store.
You should probably dispatch an action to commit the data to the store.