I have some set of components (Segment components in semantic-ui-react), where I'm trying to create a tree like component which will consist of lines as given in below diagram.
I found out a library called react-lineto but i couldn't get what i need to get. This is the code that i have tried.
import React, { Component } from "react";
import { SteppedLineTo } from "react-lineto";
import { Segment, Grid, GridColumn, GridRow } from "semantic-ui-react";
const style = {
delay: true,
borderColor: "#ddd",
borderStyle: "solid",
borderWidth: 3
};
class App extends Component {
render() {
return (
<Grid>
<GridRow>
<GridColumn width={1} />
<GridColumn width={14}>
<Segment raised compact className="A">
Comapny A
</Segment>
<Segment raised compact className="B" style={{ margin: "20px" }}>
Comapny B
</Segment>
<Segment raised compact className="C" style={{ margin: "40px" }}>
Comapny C
</Segment>
<Segment raised compact className="D" style={{ margin: "20px" }}>
Comapny D
</Segment>
<Segment raised compact className="E" style={{ margin: "0px" }}>
Company E
</Segment>
<SteppedLineTo
from="A"
to="B"
fromAnchor="left"
toAnchor="0 50%"
orientation="h"
{...style}
/>
<SteppedLineTo
from="A"
to="C"
fromAnchor="left"
toAnchor="0 50%"
orientation="h"
{...style}
/>
</GridColumn>
<GridColumn width={1} />
</GridRow>
</Grid>
);
}
}
export default App;
This renders something like this
How can i achieve this? is there any other alternative than using this library? maybe a plain css trick?.
Cool challenge!
Here is how one can do it using pure React & CSS. This solution only works for trees without loops, which seems to be your use case.
The idea is to start from a tree, enrich and flatten the nodes with all the information we need to render boxes and links, that we then place via CSS with position: absolute;
.
Hope it helps.
Here is the final output, with a box height of 20px, a gap of 10px between boxes, and a link offset of 5px (the space between the side of the box and the attach of the link).
<div style="position: relative;"><div style="position: absolute; left: 0px; top: 0px; height: 20px; border: 1px solid grey; border-radius: 2px; padding: 10px; box-sizing: border-box; display: flex; align-items: center;"><div style="position: absolute; top: 20px; left: 4px; height: 105px; border-left: 1px solid grey;"></div>Parent</div><div style="position: absolute; left: 10px; top: 30px; height: 20px; border: 1px solid grey; border-radius: 2px; padding: 10px; box-sizing: border-box; display: flex; align-items: center;"><div style="position: absolute; left: -5px; top: 4px; width: 5px; border-top: 1px solid grey;"></div><div style="position: absolute; top: 20px; left: 4px; height: 15px; border-left: 1px solid grey;"></div>Child 1</div><div style="position: absolute; left: 20px; top: 60px; height: 20px; border: 1px solid grey; border-radius: 2px; padding: 10px; box-sizing: border-box; display: flex; align-items: center;"><div style="position: absolute; left: -5px; top: 4px; width: 5px; border-top: 1px solid grey;"></div>Grandchild 1</div><div style="position: absolute; left: 10px; top: 90px; height: 20px; border: 1px solid grey; border-radius: 2px; padding: 10px; box-sizing: border-box; display: flex; align-items: center;"><div style="position: absolute; left: -5px; top: 4px; width: 5px; border-top: 1px solid grey;"></div>Child 2</div><div style="position: absolute; left: 10px; top: 120px; height: 20px; border: 1px solid grey; border-radius: 2px; padding: 10px; box-sizing: border-box; display: flex; align-items: center;"><div style="position: absolute; left: -5px; top: 4px; width: 5px; border-top: 1px solid grey;"></div><div style="position: absolute; top: 20px; left: 4px; height: 45px; border-left: 1px solid grey;"></div>Child 3</div><div style="position: absolute; left: 20px; top: 150px; height: 20px; border: 1px solid grey; border-radius: 2px; padding: 10px; box-sizing: border-box; display: flex; align-items: center;"><div style="position: absolute; left: -5px; top: 4px; width: 5px; border-top: 1px solid grey;"></div>Grandchild 2</div><div style="position: absolute; left: 20px; top: 180px; height: 20px; border: 1px solid grey; border-radius: 2px; padding: 10px; box-sizing: border-box; display: flex; align-items: center;"><div style="position: absolute; left: -5px; top: 4px; width: 5px; border-top: 1px solid grey;"></div>Grandchild 3</div></div>
Code below 👇
import React from 'react'
// Example:
//
// const topLevelNode = {
// text: 'Parent',
// children: [
// {
// text: 'Child 1',
// children: [
// {
// text: 'Grandchild 1',
// },
// ],
// },
// {
// text: 'Child 2',
// },
// ],
// }
//
// flatten(enrich(topLevelNode))
//
// [
// { text: 'Parent', depth: 0, descendentsCount: 3, heightDiffWithLastDirectChild: 3 },
// { text: 'Child 1', depth: 1, descendentsCount: 1, heightDiffWithLastDirectChild: 1 },
// { text: 'Grandchild 1', depth: 2, descendentsCount: 0, heightDiffWithLastDirectChild: 0 },
// { text: 'Child 2', depth: 1, descendentsCount: 0, heightDiffWithLastDirectChild: 0 },
// ]
// Enrich nodes with information needed for render
const enrich = (node, depthOffset = 0) => {
if (!node.children) {
return {
...node,
depth: depthOffset,
descendentsCount: 0,
heightDiffWithLastDirectChild: 0,
}
}
const enrichedChildren = node.children.map((child) => enrich(child, depthOffset + 1))
const descendentsCount = node.children.length + enrichedChildren.reduce(
(acc, enrichedChild) => acc + enrichedChild.descendentsCount,
0,
)
const heightDiffWithLastDirectChild = descendentsCount - enrichedChildren[node.children.length - 1].descendentsCount
return {
...node,
children: enrichedChildren,
depth: depthOffset,
descendentsCount,
heightDiffWithLastDirectChild,
}
}
// Flatten nodes with a depth first search
const flatten = (node) => {
const { children = [], ...nodeWithoutChildren } = node
return [
{ ...nodeWithoutChildren },
...children.map((childNode) => flatten(childNode)).flat(),
]
}
const boxHeight = 20
const boxGap = 10
const linkPositionOffset = 5
const LinkedBox = ({ node, order }) => (
<div
style={{
position: 'absolute',
left: `${node.depth * boxGap}px`,
top: `${order * (boxHeight + boxGap)}px`,
height: `${boxHeight}px`,
border: '1px solid grey',
borderRadius: '2px',
padding: '10px',
boxSizing: 'border-box',
display: 'flex',
alignItems: 'center',
}}
>
{node.depth > 0 && (
<div
style={{
position: 'absolute',
left: `-${boxGap - linkPositionOffset}px`,
top: `${linkPositionOffset - 1}px`,
width: `${boxGap - linkPositionOffset}px`,
borderTop: 'solid 1px grey',
}}
/>
)}
{node.heightDiffWithLastDirectChild > 0 && (
<div
style={{
position: 'absolute',
top: `${boxHeight}px`,
left: `${linkPositionOffset - 1}px`,
height: `${boxGap + (node.heightDiffWithLastDirectChild - 1) * (boxGap + boxHeight) + linkPositionOffset}px`,
borderLeft: 'solid 1px grey',
}}
/>
)}
{node.text}
</div>
)
const Diagram = ({ topLevelNode }) => (
<div style={{ position: 'relative' }}>
{flatten(enrich(topLevelNode)).map((enrichedNode, order) => (
<LinkedBox node={enrichedNode} order={order} key={JSON.stringify(enrichedNode)} />
))}
</div>
)
export default () => (
<Diagram
topLevelNode={{
text: 'Parent',
children: [
{
text: 'Child 1',
children: [
{ text: 'Grandchild 1' },
],
},
{ text: 'Child 2' },
{
text: 'Child 3',
children: [
{ text: 'Grandchild 2' },
{ text: 'Grandchild 3' },
],
},
],
}}
/>
)