//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//
//     Sample Code
//
//!    Visualise hierarchical data with angled links.

import { data, linkColour, primaryColour } from "./angledlinks-data.js";

import KeyLines from "keylines";

const fitButton = document.querySelector("#fit");
const fitSelectionButton = document.querySelector("#fit-selection");
const secondaryChartContainer = document.querySelector("#klchart-secondary");
const secondaryChartResizeObserver = new ResizeObserver(() => {
  secondaryChart.zoom("fit");
});
secondaryChartContainer.addEventListener("transitionend", () => {
  secondaryChartResizeObserver.unobserve(secondaryChartContainer);
});

const accentColour = "#0F6BE9";
const selectionColour = "#F4D03F";
const initialSelection = ["Company 46", "Company 19"];
const commonOptions = {
  handMode: true,
  overview: false,
  navigation: false,
  iconFontFamily: "Font Awesome 5 Free",
  selectionColour: selectionColour,
  imageAlignment: {
    "fas fa-building": { e: 0.9 },
    "fas fa-store": { e: 0.85 },
    "fas fa-industry": { e: 0.85 },
    "fas fa-warehouse": { e: 0.75, dy: -4 },
  },
};

let primaryChart;
let secondaryChart;
let highlightedPrimaryPath = undefined;
// Stores the id of the selected node being dragged so we can prevent multi node dragging
let primaryNodeBeingDragged = null;

function setPrimarySelection(ids, animate = true) {
  primaryChart.selection(ids);
  return onSelectionChange(animate);
}

function onPrimaryChartClick({ id, preventDefault }) {
  preventDefault();
  const validatedItem = primaryChart.getItem(id);
  const currentSelection = primaryChart.selection();
  if (validatedItem && validatedItem.type === "node") {
    setPrimarySelection([currentSelection[currentSelection.length - 1], id]);
  } else if (id === null || validatedItem) {
    setPrimarySelection([]);
  }
}

async function loadPathIntoSecondaryChart(ids, animate) {
  if (animate) {
    secondaryChartContainer.style.removeProperty("transition");
    secondaryChartResizeObserver.observe(secondaryChartContainer);
  } else {
    secondaryChartContainer.style.transition = "none";
  }

  if (ids.length === 0) {
    secondaryChartContainer.style.width = "0px";
  } else {
    const items = ids.map((id) => {
      return primaryChart.getItem(id);
    });

    await secondaryChart.load({
      type: "LinkChart",
      items,
    });
    secondaryChart.selection(primaryChart.selection());
    await secondaryChart.layout("sequential", {
      animate: false,
      linkShape: "curved",
    });

    secondaryChartContainer.style.width = "25%";
    if (!animate) {
      requestAnimationFrame(() => {
        secondaryChart.zoom("fit");
      });
    }
  }
}

async function highlightPrimaryPath(ids) {
  if (highlightedPrimaryPath) {
    primaryChart.setProperties(
      highlightedPrimaryPath.map((id) => {
        const type = primaryChart.getItem(id).type;
        return {
          id,
          priority: 0,
          c: type === "node" ? primaryColour : linkColour,
        };
      })
    );
    highlightedPrimaryPath = undefined;
  }

  if (ids.length > 0) {
    highlightedPrimaryPath = ids;
    primaryChart.setProperties(
      ids.map((id) => ({ id, priority: 1, c: accentColour }))
    );
  }
}

function shortestPath() {
  const selectedNodes = primaryChart.selection();
  return selectedNodes.length === 2
    ? primaryChart
        .graph()
        .shortestPaths(selectedNodes[0], selectedNodes[1], { direction: "any" })
        .onePath
    : [];
}

function onSelectionChange(animate) {
  const path = shortestPath();
  highlightPrimaryPath(path);
  return loadPathIntoSecondaryChart(path, animate);
}

function onPrimaryDragStart({ preventDefault, id, type }) {
  const selectedNodes = primaryChart.selection();
  // Prevent multi node dragging when dragging one of the two selected nodes
  if (
    type === "node" &&
    selectedNodes.includes(id) &&
    selectedNodes.length > 1
  ) {
    preventDefault();
    // Enable single node dragging via 'drag-move'
    primaryNodeBeingDragged = primaryChart.getItem(id);
  }
}

// To avoid KeyLines' multi node dragging behaviour when one of the two selected nodes is being dragged,
// handle the drag manually by updating the node's position via setProperties
function onPrimaryDragMove({ x, y }) {
  if (primaryNodeBeingDragged !== null) {
    const worldDragCoords = primaryChart.worldCoordinates(x, y);
    // Update node position properties to simulate the drag
    primaryChart.setProperties({
      id: primaryNodeBeingDragged.id,
      x: worldDragCoords.x,
      y: worldDragCoords.y,
    });
  }
}

async function loadPrimaryChart() {
  primaryChart = await KeyLines.create({
    container: "klchart",
    options: {
      ...commonOptions,
      backColour: "#1B1D20",
      selectedNode: {
        ha0: {
          c: selectionColour,
          r: 30,
          w: 4,
        },
      },
      selectedLink: {},
    },
  });
  await primaryChart.load(data);
  await primaryChart.layout("sequential", {
    linkShape: "angled",
    animate: false,
  });

  primaryChart.on("click", onPrimaryChartClick);

  // Drag handlers for bypassing KeyLines' multi node selection dragging behaviour
  primaryChart.on("drag-start", onPrimaryDragStart);
  primaryChart.on("drag-move", onPrimaryDragMove);
  primaryChart.on("drag-end", () => {
    if (primaryNodeBeingDragged) {
      // Clear the node being dragged when the drag is complete
      primaryNodeBeingDragged = null;
    }
  });
}

async function loadSecondaryChart() {
  secondaryChart = await KeyLines.create({
    container: "klchart-secondary",
    options: {
      ...commonOptions,
      backColour: "#131517",
      zoom: {
        adaptiveStyling: false,
      },
    },
  });
  await loadPathIntoSecondaryChart([]);

  secondaryChart.on("click", ({ preventDefault }) => preventDefault());
  secondaryChart.on("drag-start", ({ type, preventDefault }) => {
    if (type === "node") {
      preventDefault();
    }
  });
}

async function startKeyLines() {
  await Promise.all([loadPrimaryChart(), loadSecondaryChart()]);
  await setPrimarySelection(initialSelection, false);
  requestAnimationFrame(() => {
    primaryChart.zoom("fit");
  });
  fitSelectionButton.addEventListener("click", () => {
    primaryChart.zoom("fit", { ids: shortestPath(), animate: true, time: 150 });
  });
  fitButton.addEventListener("click", () => {
    primaryChart.zoom("fit", { animate: true, time: 150 });
  });
}

function loadFontsAndStart() {
  document.fonts.load('24px "Font Awesome 5 Free"').then(startKeyLines);
}

window.addEventListener("DOMContentLoaded", loadFontsAndStart);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
  <head>
    <meta charset="UTF-8">
    <title>Path Analysis in Trees</title>
    <link rel="stylesheet" type="text/css" href="/css/keylines.css">
    <link rel="stylesheet" type="text/css" href="/css/minimalsdk.css">
    <link rel="stylesheet" type="text/css" href="/css/sdk-layout.css">
    <link rel="stylesheet" type="text/css" href="/css/demo.css">
    <link rel="stylesheet" type="text/css" href="/css/fontawesome5/fontawesome.css">
    <link rel="stylesheet" type="text/css" href="/css/fontawesome5/solid.css">
    <link rel="stylesheet" type="text/css" href="/angledlinks.css">
    <script src="/vendor/jquery.js" defer type="text/javascript"></script>
    <script id="keylines" src="/public/keylines.umd.cjs" defer type="text/javascript"></script>
    <script src="/angledlinks.js" crossorigin="use-credentials" defer type="module"></script>
  </head>
  <body>
    <div class="chart-wrapper demo-cols">
      <div class="tab-content-panel flex1" id="lhs" data-tab-group="rhs">
        <div class="toggle-content is-visible" id="lhsVisual" style="width:100%; height: 100%;">
          <div id="charts">
            <div class="klchart" id="klchart">
              <div id="primary-chart-controls">  
                <button id="fit"> <img src="/im/up-right-and-down-left-from-center.svg" alt="Fit"></button>
                <button id="fit-selection"><img src="/im/location-crosshairs.svg" alt="Fit to selection"></button>
              </div>
            </div>
            <div class="klchart" id="klchart-secondary"></div>
          </div>
        </div>
      </div>
      <div class="rhs citext closed" id="demorhscontent">
        <div class="title-bar">
          <svg viewBox="0 0 360 60" style="max-width: 360px; max-height: 60px; font-size: 24px; font-family: Raleway; flex: 1;">
            <text x="0" y="38" style="width: 100%;">Path Analysis in Trees</text>
          </svg>
        </div>
        <div class="tab-content-panel" data-tab-group="rhs">
          <div class="toggle-content is-visible tabcontent" id="controlsTab">
            <form autocomplete="off" onsubmit="return false" id="rhsForm">
              <div class="cicontent">
                <fieldset>
                  <p>Angled links help visualise complex hierarchical data, while multiple charts can be used alongside each other to summarise specific information:</p>
                  <ul>
                    <li>
                      Click two nodes in the main chart to highlight the shortest path between them.
                      This path is summarised in the second chart, to the right.
                    </li>
                    <li>Click the main chart to deselect everything.</li>
                  </ul>
                  <p>
                    Use the controls at the top left to either see the whole main chart, or just its selected nodes.
                    
                  </p>
                </fieldset>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
    <div id="moreParent">
      <div id="moreContainer">
      </div>
    </div>
    <div id="lazyScripts">
    </div>
  </body>
</html>
#charts {
  display: flex;
  width: 100%;
  height: 100%;
}

#klchart {
  background-color: #1B1D1F;
}

#klchart-secondary {
  width: 0px;
  transition: width 0.25s;
  display: flex;
  align-items: center;
  justify-content: center;
}

#primary-chart-controls {
  background-color: #242528;
  display: flex;
  position: absolute;
  left: 10px;
  top: 10px;
  padding: 10px;
  gap: 10px;
  border-radius: 6px;
}

#primary-chart-controls > button {
  background-color: #34373e;
  padding: 10px;
  border-radius: 6px !important;
}

#primary-chart-controls > button:hover {
  background-color: #0f6be9;
}
Loading source