Search code examples
htmlcssreactjstreeview

How to draw line between 2 components


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.

enter image description here

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

enter image description here

How can i achieve this? is there any other alternative than using this library? maybe a plain css trick?.


Solution

  • 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' },
              ],
            },
          ],
        }}
      />
    )