//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//
//     Sample Code
//
//!    Use rectangular combos to visualise IT networks.
import KeyLines from "keylines";
import { data } from "./arrangingitnetworks-data.js";

let chart;
let graph;

// Style info
const backgroundColour = "#1f1f1f";
const alertColour = "#e5309a";
const selectedColour = "white";
const linkColour = "#454545";
const topLevelLinkColour = "#39bffd";

// State tracking
let alwaysRevealedLinks = [];

// Find all the links that have alerts attached.
function getAlertLinks() {
  const alerts = [];
  chart.each({ type: "link" }, (link) => {
    if (link.d.alert) {
      alerts.push(link.id);
    }
  });
  return alerts;
}

function decorateAlertLinks(linkIds) {
  chart.setProperties(
    linkIds.map((id) => ({
      id,
      c: alertColour,
      g: [
        {
          c: alertColour,
          b: null,
          fi: { t: "fas fa-exclamation", c: "white" },
          e: 1.5,
        },
      ],
    }))
  );
}

function showTraffic(linkIds) {
  const toForeground = {};
  const newLinkProperties = linkIds.map((id) => {
    toForeground[id] = true;
    const link = chart.getItem(id);
    const c = link.d.alert ? alertColour : selectedColour;
    return {
      id,
      c,
      flow: link.d.flow,
    };
  });
  chart.setProperties(newLinkProperties);
  const reveal = alwaysRevealedLinks.concat(linkIds);
  chart.combo().reveal(reveal);
  chart.foreground((link) => toForeground[link.id], {
    type: "link",
    items: "underlying",
  });
}

function hideTraffic() {
  const revealedIds = chart.combo().reveal();
  chart.setProperties(
    revealedIds.map((id) => {
      const link = chart.getItem(id);
      return {
        id: link.id,
        c: link.d.alert ? alertColour : linkColour,
        flow: false,
      };
    })
  );
  chart.combo().reveal(alwaysRevealedLinks);
}

function select(selections) {
  const itemId =
    selections.length > 0 ? selections[selections.length - 1] : null;
  const item = chart.getItem(itemId);
  if (item && item.type === "node" && !chart.combo().isCombo(itemId)) {
    showTraffic(graph.neighbours(itemId, { all: true }).links);
  } else {
    chart.foreground(() => true);
  }
}

async function onDoubleClick({ id, preventDefault, button }) {
  if (!id || button !== 0) {
    return;
  }

  const api = chart.combo();
  const combo = api.isCombo(id) ? id : chart.getItem(id).parentId;
  const opts = { adapt: "inCombo" };

  if (api.isCombo(combo)) {
    preventDefault();
    if (api.isOpen(combo)) {
      await api.close(combo, opts);
    } else {
      await api.open(combo, opts);
    }
    layout();
  }
}

function initialiseInteractions() {
  // perform layout on combo open/close
  chart.on("double-click", onDoubleClick);

  // reveal and foreground selected nodes and links
  chart.on("selection-change", () => {
    hideTraffic();
    select(chart.selection());
  });
}

// generate a list of all combos ordered by how deeply nested they are
function getComboList() {
  const comboIds = [];
  chart.each({ type: "node", items: "all" }, ({ id }) => {
    if (chart.combo().isCombo(id)) {
      comboIds.push(id);
    }
  });
  return comboIds;
}

function getInnerCombos() {
  const combo = chart.combo();
  return getComboList().filter((id) => {
    let isInnerCombo = true;
    // is not an inner combo if any child is a combo
    chart.each({ items: "all", type: "node" }, (item) => {
      if (item.parentId === id && combo.isCombo(item.id)) {
        isInnerCombo = false;
      }
    });
    return isInnerCombo;
  });
}

async function openAllCombos() {
  const allCombos = getComboList();
  const innerCombos = getInnerCombos();
  const outerCombos = allCombos.filter((id) => !innerCombos.includes(id));
  await chart.combo().open(allCombos, { animate: false });
  await chart.combo().arrange(outerCombos, {
    name: "grid",
    animate: false,
  });
  await chart.combo().arrange(innerCombos, {
    name: "grid",
    animate: false,
    tightness: 3,
  });
}

function colourTopLevelLinks() {
  chart.each({ type: "link", items: "toplevel" }, ({ id }) => {
    chart.setProperties({ id, c: topLevelLinkColour });
  });
}

async function startKeyLines() {
  const options = {
    backColour: backgroundColour,
    backgroundAlpha: 0.45,
    combos: {
      shape: "rectangle",
    },
    controlTheme: "dark",
    selectedLink: {},
    selectedNode: {
      b: selectedColour,
      bw: 3,
    },
    iconFontFamily: "Font Awesome 5 Free",
    handMode: true,
    defaultStyles: {
      comboGlyph: {
        p: 45,
      },
    },
    imageAlignment: {
      "fas fa-print": { e: 0.8 },
      "fas fa-laptop": { e: 0.75 },
      "fas fa-phone": { e: 0.8 },
      "fas fa-server": { e: 0.8 },
      "fas fa-sitemap": { e: 0.75, dy: -8 },
    },
  };

  chart = await KeyLines.create({ container: "klchart", options });

  initialiseInteractions();

  // Use a graph engine to track relations in the underlying data
  graph = KeyLines.getGraphEngine();
  graph.load(data);

  chart.load(data);

  // reveal the links with alerts attached and make sure they stay that way
  alwaysRevealedLinks = getAlertLinks();
  chart.combo().reveal(alwaysRevealedLinks);

  // also style them dramatically...
  decorateAlertLinks(alwaysRevealedLinks);
  await openAllCombos();
  colourTopLevelLinks();
  layout();
}

function layout() {
  chart.layout("sequential", { level: "level", linkShape: "curved" });
}

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>Arranging IT Networks</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="/arrangingitnetworks.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="/arrangingitnetworks.js" crossorigin="use-credentials" 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 class="klchart" id="klchart">
          </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%;">Arranging IT Networks</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">
                <p>Double-click the combos to toggle their state.</p>
                <p>Click on a node to see its connections to the rest of the network.</p>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
    <div id="moreParent">
      <div id="moreContainer">
      </div>
    </div>
    <div id="lazyScripts">
    </div>
  </body>
</html>
.klchart {
  border: none;
  background-color:#1f1f1f;
}
Loading source