Search

Angled Links

Layouts

Visualize hierarchical data with angled links.

Angled Links
View live example →

Relationships between nodes that represent a large data hierarchy can be shown effectively using angled links. These reflect the tree-like structure of the data so that relationships can be visualized more clearly.

Click two nodes in the chart to highlight and view the shortest path between them.

Click the chart background to deselect everything.

See also

import React, { useEffect, useRef, useState } from "react";
import { createRoot } from "react-dom/client";

import forEach from "lodash/forEach";
import { Chart } from "regraph";
import { shortestPaths } from "regraph/analysis";

import { items } from "./data";
import { style } from "@ci/theme/rg/js/storyStyles";

import "@fortawesome/fontawesome-free/css/fontawesome.css";
import "@fortawesome/fontawesome-free/css/solid.css";
import "@ci/theme/rg/css/button.css";

const accentColor = style.colors.teal2;
const selectionColor = style.colors.red2;
const options = {
  backgroundColor: "white",
  handMode: true,
  overview: false,
  navigation: false,
  iconFontFamily: "Font Awesome 5 Free",
  selection: { color: selectionColor },
  imageAlignment: {
    "fas fa-building": { size: 0.9 },
    "fas fa-store": { size: 0.85 },
    "fas fa-industry": { size: 0.85 },
    "fas fa-warehouse": { size: 0.75, dy: -4 },
  },
};
const layout = { name: "sequential", linkShape: "angled" };
const initialPathEnds = ["Company 46", "Company 19"];

function getStyledItems(allItems, path) {
  const styledItems = { ...allItems };

  forEach(path.items, (item, id) => {
    styledItems[id] = {
      ...item,
      priority: 1,
      color: accentColor,
    };
  });

  [0, 1].forEach((i) => {
    if (path.ends[i] !== null) {
      styledItems[path.ends[i]].halos = [
        {
          color: selectionColor,
          radius: 30,
          width: 4,
        },
      ];
    }
  });

  return styledItems;
}

function AngledLinks() {
  const [path, setPath] = useState({
    ends: [null, null],
    items: {},
  });
  const [selectedIds, setSelectedIds] = useState([]);
  const chartRef = useRef(null);

  useEffect(() => {
    const setInitialPath = async () => {
      const initialPath = await shortestPaths(items, initialPathEnds[0], initialPathEnds[1]);
      setPath({
        ends: initialPathEnds,
        items: initialPath.items,
      });
    };
    setInitialPath();
  }, [items]);

  const fit = (fitType) => {
    if (chartRef.current !== null) {
      chartRef.current.fit(fitType);
    }
  };

  async function onChangeHandler({ selection }) {
    // prevent the selection of more than one node
    setSelectedIds([]);

    if (selection === undefined) {
      return;
    }

    const selectedId = Object.keys(selection)[0];

    if (path.ends.indexOf(selectedId) === -1) {
      const validatedItem = items[selectedId];

      // check the item is a node
      if (validatedItem && validatedItem.id1 === undefined) {
        const newPathEnds = [path.ends[1], selectedId];
        const newPath =
          newPathEnds[0] !== null
            ? await shortestPaths(items, newPathEnds[0], newPathEnds[1])
            : { items: { [selectedId]: validatedItem } };

        setPath({
          ends: newPathEnds,
          items: newPath.items,
        });
      }
    }
  }

  function onClickHandler({ id }) {
    if (id === null) {
      setPath({
        ends: [null, null],
        items: {},
      });
    }
  }

  return (
    <div className="story">
      <div className="options">
        <button type="button" onClick={() => fit("all")}>
          Fit Chart
        </button>
        <button type="button" onClick={() => fit(path.items)}>
          Fit Selection
        </button>
      </div>
      <div className="chart-wrapper">
        <Chart
          ref={chartRef}
          items={                           }
          selection={selectedIds}
          layout={layout}
          options={options}
          animation={{ animate: false }}
          onChange={onChangeHandler}
          onClick={onClickHandler}
        />
      </div>
    </div>
  );
}

const FontReadyChart = React.lazy(() =>
  document.fonts.load("900 24px 'Font Awesome 5 Free'").then(() => ({
    default: AngledLinks,
  }))
);

export function Demo() {
  return (
    <React.Suspense fallback="">
      <FontReadyChart />
    </React.Suspense>
  );
}

const root = createRoot(document.getElementById("regraph"));
root.render(<Demo />);
import { style } from "@ci/theme/rg/js/storyStyles";

const primaryColor = style.colors.gradient00;
const textColor = style.colors.teal2;
const fontIconColor = style.colors.pureWhite;
const icons = ["fas fa-warehouse", "fas fa-store", "fas fa-industry", "fas fa-building"];
const linkColor = style.colors.gradient00;
const linkWidth = 3;

function makeNode(id, icon) {
  return {
    label: [
      {
        text: id,
        position: "ne",
        color: textColor,
        backgroundColor: "rgba(0,0,0,0)",
      },
    ],
    color: primaryColor,
    fontIcon: {
      text: icon,
      color: fontIconColor,
    },
    data: {
      type: "company",
    },
  };
}

function makeLink(id1, id2) {
  return {
    id1,
    id2,
    color: linkColor,
    width: linkWidth,
    end2: { arrow: true },
  };
}

export const items = {
  "Company 85-Company 74": makeLink("Company 85", "Company 74"),
  "Company 34-Company 49": makeLink("Company 34", "Company 49"),
  "Company 16-Company 51": makeLink("Company 16", "Company 51"),
  "Company 42-Company 36": makeLink("Company 42", "Company 36"),
  "Company 98-Company 67": makeLink("Company 98", "Company 67"),
  "Company 4-Company 48": makeLink("Company 4", "Company 48"),
  "Company 64-Company 1": makeLink("Company 64", "Company 1"),
  "Company 45-Company 50": makeLink("Company 45", "Company 50"),
  "Company 13-Company 23": makeLink("Company 13", "Company 23"),
  "Company 37-Company 39": makeLink("Company 37", "Company 39"),
  "Company 35-Company 83": makeLink("Company 35", "Company 83"),
  "Company 12-Company 27": makeLink("Company 12", "Company 27"),
  "Company 87-Company 38": makeLink("Company 87", "Company 38"),
  "Company 52-Company 79": makeLink("Company 52", "Company 79"),
  "Company 32-Company 53": makeLink("Company 32", "Company 53"),
  "Company 46-Company 76": makeLink("Company 46", "Company 76"),
  "Company 98-Company 93": makeLink("Company 98", "Company 93"),
  "Company 95-Company 45": makeLink("Company 95", "Company 45"),
  "Company 57-Company 75": makeLink("Company 57", "Company 75"),
  "Company 35-Company 62": makeLink("Company 35", "Company 62"),
  "Company 94-Company 84": makeLink("Company 94", "Company 84"),
  "Company 42-Company 90": makeLink("Company 42", "Company 90"),
  "Company 72-Company 70": makeLink("Company 72", "Company 70"),
  "Company 83-Company 60": makeLink("Company 83", "Company 60"),
  "Company 79-Company 32": makeLink("Company 79", "Company 32"),
  "Company 19-Company 59": makeLink("Company 19", "Company 59"),
  "Company 16-Company 100": makeLink("Company 16", "Company 100"),
  "Company 4-Company 81": makeLink("Company 4", "Company 81"),
  "Company 17-Company 20": makeLink("Company 17", "Company 20"),
  "Company 40-Company 95": makeLink("Company 40", "Company 95"),
  "Company 72-Company 56": makeLink("Company 72", "Company 56"),
  "Company 81-Company 46": makeLink("Company 81", "Company 46"),
  "Company 48-Company 28": makeLink("Company 48", "Company 28"),
  "Company 54-Company 6": makeLink("Company 54", "Company 6"),
  "Company 83-Company 69": makeLink("Company 83", "Company 69"),
  "Company 72-Company 98": makeLink("Company 72", "Company 98"),
  "Company 94-Company 87": makeLink("Company 94", "Company 87"),
  "Company 23-Company 37": makeLink("Company 23", "Company 37"),
  "Company 95-Company 17": makeLink("Company 95", "Company 17"),
  "Company 96-Company 44": makeLink("Company 96", "Company 44"),
  "Company 86-Company 57": makeLink("Company 86", "Company 57"),
  "Company 19-Company 26": makeLink("Company 19", "Company 26"),
  "Company 81-Company 65": makeLink("Company 81", "Company 65"),
  "Company 73-Company 99": makeLink("Company 73", "Company 99"),
  "Company 96-Company 43": makeLink("Company 96", "Company 43"),
  "Company 60-Company 54": makeLink("Company 60", "Company 54"),
  "Company 48-Company 94": makeLink("Company 48", "Company 94"),
  "Company 60-Company 68": makeLink("Company 60", "Company 68"),
  "Company 73-Company 41": makeLink("Company 73", "Company 41"),
  "Company 17-Company 18": makeLink("Company 17", "Company 18"),
  "Company 64-Company 19": makeLink("Company 64", "Company 19"),
  "Company 81-Company 13": makeLink("Company 81", "Company 13"),
  "Company 42-Company 91": makeLink("Company 42", "Company 91"),
  "Company 6-Company 72": makeLink("Company 6", "Company 72"),
  "Company 78-Company 15": makeLink("Company 78", "Company 15"),
  "Company 97-Company 78": makeLink("Company 97", "Company 78"),
  "Company 82-Company 35": makeLink("Company 82", "Company 35"),
  "Company 78-Company 92": makeLink("Company 78", "Company 92"),
  "Company 46-Company 82": makeLink("Company 46", "Company 82"),
  "Company 13-Company 47": makeLink("Company 13", "Company 47"),
  "Company 100-Company 7": makeLink("Company 100", "Company 7"),
  "Company 73-Company 4": makeLink("Company 73", "Company 4"),
  "Company 73-Company 97": makeLink("Company 73", "Company 97"),
  "Company 75-Company 30": makeLink("Company 75", "Company 30"),
  "Company 50-Company 64": makeLink("Company 50", "Company 64"),
  "Company 77-Company 34": makeLink("Company 77", "Company 34"),
  "Company 6-Company 58": makeLink("Company 6", "Company 58"),
  "Company 26-Company 31": makeLink("Company 26", "Company 31"),
  "Company 99-Company 73": makeLink("Company 99", "Company 73"),
  "Company 2-Company 55": makeLink("Company 2", "Company 55"),
  "Company 43-Company 42": makeLink("Company 43", "Company 42"),
  "Company 78-Company 12": makeLink("Company 78", "Company 12"),
  "Company 94-Company 86": makeLink("Company 94", "Company 86"),
  "Company 31-Company 9": makeLink("Company 31", "Company 9"),
  "Company 92-Company 29": makeLink("Company 92", "Company 29"),
  "Company 43-Company 10": makeLink("Company 43", "Company 10"),
  "Company 77-Company 85": makeLink("Company 77", "Company 85"),
  "Company 6-Company 16": makeLink("Company 6", "Company 16"),
  "Company 47-Company 25": makeLink("Company 47", "Company 25"),
  "Company 44-Company 52": makeLink("Company 44", "Company 52"),
  "Company 15-Company 71": makeLink("Company 15", "Company 71"),
  "Company 16-Company 24": makeLink("Company 16", "Company 24"),
  "Company 94-Company 61": makeLink("Company 94", "Company 61"),
  "Company 81-Company 96": makeLink("Company 81", "Company 96"),
  "Company 15-Company 77": makeLink("Company 15", "Company 77"),
  "Company 1-Company 11": makeLink("Company 1", "Company 11"),
  "Company 86-Company 2": makeLink("Company 86", "Company 2"),
  "Company 96-Company 40": makeLink("Company 96", "Company 40"),
  "Company 53-Company 63": makeLink("Company 53", "Company 63"),
  "Company 85": makeNode("Company 85", icons[85 % icons.length]),
  "Company 74": makeNode("Company 74", icons[74 % icons.length]),
  "Company 34": makeNode("Company 34", icons[34 % icons.length]),
  "Company 49": makeNode("Company 49", icons[49 % icons.length]),
  "Company 16": makeNode("Company 16", icons[16 % icons.length]),
  "Company 51": makeNode("Company 51", icons[51 % icons.length]),
  "Company 42": makeNode("Company 42", icons[42 % icons.length]),
  "Company 36": makeNode("Company 36", icons[36 % icons.length]),
  "Company 98": makeNode("Company 98", icons[98 % icons.length]),
  "Company 67": makeNode("Company 67", icons[67 % icons.length]),
  "Company 4": makeNode("Company 4", icons[4 % icons.length]),
  "Company 48": makeNode("Company 48", icons[48 % icons.length]),
  "Company 64": makeNode("Company 64", icons[64 % icons.length]),
  "Company 1": makeNode("Company 1", icons[1 % icons.length]),
  "Company 45": makeNode("Company 45", icons[45 % icons.length]),
  "Company 50": makeNode("Company 50", icons[50 % icons.length]),
  "Company 13": makeNode("Company 13", icons[13 % icons.length]),
  "Company 23": makeNode("Company 23", icons[23 % icons.length]),
  "Company 37": makeNode("Company 37", icons[37 % icons.length]),
  "Company 39": makeNode("Company 39", icons[39 % icons.length]),
  "Company 35": makeNode("Company 35", icons[35 % icons.length]),
  "Company 83": makeNode("Company 83", icons[83 % icons.length]),
  "Company 12": makeNode("Company 12", icons[12 % icons.length]),
  "Company 27": makeNode("Company 27", icons[27 % icons.length]),
  "Company 87": makeNode("Company 87", icons[87 % icons.length]),
  "Company 38": makeNode("Company 38", icons[38 % icons.length]),
  "Company 52": makeNode("Company 52", icons[52 % icons.length]),
  "Company 79": makeNode("Company 79", icons[79 % icons.length]),
  "Company 32": makeNode("Company 32", icons[32 % icons.length]),
  "Company 53": makeNode("Company 53", icons[53 % icons.length]),
  "Company 46": makeNode("Company 46", icons[46 % icons.length]),
  "Company 76": makeNode("Company 76", icons[76 % icons.length]),
  "Company 93": makeNode("Company 93", icons[93 % icons.length]),
  "Company 95": makeNode("Company 95", icons[95 % icons.length]),
  "Company 57": makeNode("Company 57", icons[57 % icons.length]),
  "Company 75": makeNode("Company 75", icons[75 % icons.length]),
  "Company 62": makeNode("Company 62", icons[62 % icons.length]),
  "Company 94": makeNode("Company 94", icons[94 % icons.length]),
  "Company 84": makeNode("Company 84", icons[84 % icons.length]),
  "Company 90": makeNode("Company 90", icons[90 % icons.length]),
  "Company 72": makeNode("Company 72", icons[72 % icons.length]),
  "Company 70": makeNode("Company 70", icons[70 % icons.length]),
  "Company 60": makeNode("Company 60", icons[60 % icons.length]),
  "Company 19": makeNode("Company 19", icons[19 % icons.length]),
  "Company 59": makeNode("Company 59", icons[59 % icons.length]),
  "Company 100": makeNode("Company 100", icons[100 % icons.length]),
  "Company 81": makeNode("Company 81", icons[81 % icons.length]),
  "Company 17": makeNode("Company 17", icons[17 % icons.length]),
  "Company 20": makeNode("Company 20", icons[20 % icons.length]),
  "Company 40": makeNode("Company 40", icons[40 % icons.length]),
  "Company 56": makeNode("Company 56", icons[56 % icons.length]),
  "Company 28": makeNode("Company 28", icons[28 % icons.length]),
  "Company 54": makeNode("Company 54", icons[54 % icons.length]),
  "Company 6": makeNode("Company 6", icons[6 % icons.length]),
  "Company 69": makeNode("Company 69", icons[69 % icons.length]),
  "Company 96": makeNode("Company 96", icons[96 % icons.length]),
  "Company 44": makeNode("Company 44", icons[44 % icons.length]),
  "Company 86": makeNode("Company 86", icons[86 % icons.length]),
  "Company 26": makeNode("Company 26", icons[26 % icons.length]),
  "Company 65": makeNode("Company 65", icons[65 % icons.length]),
  "Company 73": makeNode("Company 73", icons[73 % icons.length]),
  "Company 99": makeNode("Company 99", icons[99 % icons.length]),
  "Company 43": makeNode("Company 43", icons[43 % icons.length]),
  "Company 68": makeNode("Company 68", icons[68 % icons.length]),
  "Company 41": makeNode("Company 41", icons[41 % icons.length]),
  "Company 18": makeNode("Company 18", icons[18 % icons.length]),
  "Company 91": makeNode("Company 91", icons[91 % icons.length]),
  "Company 78": makeNode("Company 78", icons[78 % icons.length]),
  "Company 15": makeNode("Company 15", icons[15 % icons.length]),
  "Company 97": makeNode("Company 97", icons[97 % icons.length]),
  "Company 82": makeNode("Company 82", icons[82 % icons.length]),
  "Company 92": makeNode("Company 92", icons[92 % icons.length]),
  "Company 47": makeNode("Company 47", icons[47 % icons.length]),
  "Company 7": makeNode("Company 7", icons[7 % icons.length]),
  "Company 30": makeNode("Company 30", icons[30 % icons.length]),
  "Company 77": makeNode("Company 77", icons[77 % icons.length]),
  "Company 58": makeNode("Company 58", icons[58 % icons.length]),
  "Company 31": makeNode("Company 31", icons[31 % icons.length]),
  "Company 2": makeNode("Company 2", icons[2 % icons.length]),
  "Company 55": makeNode("Company 55", icons[55 % icons.length]),
  "Company 9": makeNode("Company 9", icons[9 % icons.length]),
  "Company 29": makeNode("Company 29", icons[29 % icons.length]),
  "Company 10": makeNode("Company 10", icons[10 % icons.length]),
  "Company 25": makeNode("Company 25", icons[25 % icons.length]),
  "Company 71": makeNode("Company 71", icons[71 % icons.length]),
  "Company 24": makeNode("Company 24", icons[24 % icons.length]),
  "Company 61": makeNode("Company 61", icons[61 % icons.length]),
  "Company 11": makeNode("Company 11", icons[11 % icons.length]),
  "Company 63": makeNode("Company 63", icons[63 % icons.length]),
};
<!doctype html>
<html>
  <body>
    <div id="regraph" style="height: 100vh"></div>
    <script type="module" src="./code.jsx"></script>
  </body>
</html>

Terms of use

These terms do not alter or supersede any existing agreements between you (or your employer) and us.

By accessing or using any Content you agree to be bound by these Terms of Use. Please review these terms carefully before using the website.

The contents of this website, including but not limited to any text, code samples, API references, schemas, interactive tools, and other materials (collectively, the 'Content'), are made available for informational and internal evaluation purposes only. All intellectual property rights in the Content are reserved. No licence is granted to use the Content for any commercial purpose, or to copy, distribute, modify, reverse-engineer, or incorporate any part of the Content into any product or service, without our prior written consent.

This Content is provided “as is” and “as available,” without any representations, warranties, or guarantees of any kind, whether express or implied, including but not limited to implied warranties of merchantability, fitness for a particular purpose, non-infringement, or accuracy. To the fullest extent permitted by applicable law, we expressly exclude and disclaim all implied warranties, conditions, and other terms that might otherwise be implied.

We disclaim all liability for any loss or damage, whether direct, indirect, incidental, consequential, or otherwise, arising from any reliance placed on the Content or from your use of it, to the fullest extent permitted by applicable law. By continuing to access or use the Content, you acknowledge and agree to these terms.