//
//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//
//     Sample Code
//!    Reveal the dependencies and significance of nodes.
import KeyLines from "keylines";
import {
  data,
  fontIcons,
  errorColour,
  warningColour,
  dimmedColour,
  bgColour,
  annotationsState,
} from "./impact-data.js";

// declaring global vars
let chart;
let impactAlertsSelection = document.querySelector(
  'input[name="dependencyView"]:checked'
).value;
let showNodes = null;
let selectedNodeId = null;
const squareNodeText = {};
const rectangleNodeText = {};
const initialNodeStyles = [];
const alertsItems = [];
let hoveredGlyphId = null;
let hoveredAnnotationGlyphId = null;

const graph = KeyLines.getGraphEngine();
graph.load(data);

const impactAlertsRadios = Array.from(
  document.querySelectorAll('input[name="dependencyView"]')
);
const alertsList = document.getElementById("alerts");
const resetButton = document.getElementById("reset");

function getScaleFactor(numOfDeps) {
  return numOfDeps ** (1 / 3);
}

function highlightErrorsAndWarnings() {
  const colourItems = [];
  const warnings = [];
  let errors = [];

  chart.each({ type: "node" }, (item) => {
    if (item.d.logLevel === "error") {
      errors.push(item.id);
      let neighbours = graph.neighbours(item.id, { direction: "from", hops: 5 })
        .nodes;
      errors = errors.concat(neighbours);
    } else if (item.d.logLevel !== "warning") {
      colourItems.push({ id: item.id, c: dimmedColour });
    }
  });
  chart.each({ type: "node" }, (item) => {
    if (item.d.logLevel === "warning") {
      warnings.push(item.id);
      let neighbours = graph.neighbours(item.id, { direction: "from", hops: 5 })
        .nodes;
      neighbours.forEach((id) => {
        if (!errors.includes(id)) {
          warnings.push(id);
        }
      });
    }
  });
  errors.forEach((id) => colourItems.push({ id, c: errorColour }));
  warnings.forEach((id) => colourItems.push({ id, c: warningColour }));

  chart.each({ type: "link" }, (item) => {
    if (errors.includes(item.id2)) {
      colourItems.push({ id: item.id, c: errorColour, c2: errorColour });
    } else if (warnings.includes(item.id2)) {
      colourItems.push({ id: item.id, c: warningColour, c2: warningColour });
    } else {
      colourItems.push({ id: item.id, c: dimmedColour, c2: dimmedColour });
    }
  });
  chart.setProperties(colourItems);
  applyAnnotationStateUpdateToChart();
}

function applyAnnotationStateUpdateToChart() {
  const annotations = [];
  for (const [_, value] of Object.entries(annotationsState)) {
    if (value.hidden) continue;
    annotations.push(value.annotation);
  }
  chart.merge(annotations);
}

function removeAnnotationFromChart(id) {
  chart.removeItem(id);
}

function constructTable() {
  const tableBody = document.querySelector(".alertsTableBody");
  alertsItems.forEach((item) => {
    const label = document.createElement("td");
    const labelText = document.createTextNode(item.label);
    label.appendChild(labelText);

    const type = document.createElement("td");
    const typeText = document.createTextNode(
      item.type.charAt(0).toUpperCase() + item.type.slice(1)
    );
    type.appendChild(typeText);

    const description = document.createElement("td");
    const descriptionText = document.createTextNode(item.description);
    description.appendChild(descriptionText);

    const row = document.createElement("tr");
    row.id = item.nodeId;
    row.setAttribute("name", "alertRow");
    row.appendChild(label);
    row.appendChild(type);
    row.appendChild(description);
    row.onclick = function () {
      setSelection({ id: item.nodeId });
    };

    tableBody.appendChild(row);
  });
}

function clearTableStyling() {
  const alertsTableRows = document.querySelectorAll('tr[name="alertRow"]');
  alertsTableRows.forEach((row) => {
    row.classList.remove("selected-row");
  });
}

function showHideAlertsList() {
  impactAlertsSelection = document.querySelector(
    'input[name="dependencyView"]:checked'
  ).value;

  if (impactAlertsSelection === "impact") {
    chart.each({ type: "annotation" }, (annotation) => {
      chart.removeItem(annotation.id);
    });
    chart.setProperties(initialNodeStyles);
    alertsList.classList.add("hidden");
  } else {
    highlightErrorsAndWarnings();
    alertsList.classList.remove("hidden");
  }
}

async function applyFilters() {
  chart.lock(true);
  const result = await chart.filter(
    (item) => {
      if (item.type === "node" && showNodes) {
        return showNodes[item.id] !== undefined;
      }
      return true;
    },
    { time: 300 }
  );
  chart.lock(false);
  return result;
}

async function doLayout() {
  const layoutOptions = {
    consistent: true,
    mode: "adaptive",
  };
  let layoutName;
  // A node needs to be selected for the selected layout to be run
  if (selectedNodeId) {
    // Set the top node to be the selected one
    layoutName = "sequential";

    // Increase the space between levels for gentler curves in the links
    // but reduce the space between nodes within the same level
    layoutOptions.stretch = 2;
    layoutOptions.tightness = 8;
    layoutOptions.orientation = "right";
    layoutOptions.linkShape = "curved";
  } else {
    layoutName = "organic";
    layoutOptions.tightness = 6;
  }
  await chart.layout(layoutName, layoutOptions);
}

function hoverReaction({ id, subItem }) {
  const item = chart.getItem(id);
  const isAnnotation = id && id.includes("annotation");
  const changes = [];

  // Clear any existing hover styling
  if (hoveredGlyphId) {
    const node = chart.getItem(hoveredGlyphId);
    chart.setProperties({
      id: hoveredGlyphId,
      g: [
        {
          ...node.g[0],
          border: {
            width: 1.5,
            colour: bgColour,
          },
        },
      ],
    });
    hoveredGlyphId = null;
  }

  if (hoveredAnnotationGlyphId) {
    const annotation = chart.getItem(hoveredAnnotationGlyphId);
    if (annotation) {
      changes.push({
        id: hoveredAnnotationGlyphId,
        g: [
          annotation.g[0],
          {
            ...annotation.g[1],
            border: {
              width: 1.5,
              colour: bgColour,
            },
          },
        ],
      });
    }
    hoveredAnnotationGlyphId = null;
  }

  // Add any new glyph hover styling
  if (subItem.type === "glyph" && !isAnnotation) {
    hoveredGlyphId = id;
    chart.setProperties({
      id,
      g: [
        {
          ...item.g[0],
          border: {
            width: 1.5,
            colour: "white",
          },
        },
      ],
    });
  }

  if (isAnnotation && subItem.type === "glyph" && subItem.index === 1) {
    chart.setProperties({
      id,
      g: [
        item.g[0],
        {
          ...item.g[1],
          border: {
            width: 1.5,
            colour: "white",
          },
        },
      ],
    });
    hoveredAnnotationGlyphId = id;
    return;
  }

  // Handle node + node's tree hover styling and clearing
  const tree =
    item && item.type === "node"
      ? graph.distances(id, { direction: "from" })
      : [];
  chart.each({ type: "node" }, (node) => {
    // don't overwrite selected node styling
    if (node.id !== selectedNodeId) {
      const highlight = node.id in tree && !hoveredGlyphId;
      const borderWidth = node.w === 300 ? node.w / 40 : node.w / 20;
      changes.push({
        id: node.id,
        b: highlight ? "white" : undefined,
        bw: highlight ? borderWidth : 0,
      });
    }
  });

  chart.setProperties(changes);
}

async function setSelection({ id, subItem }) {
  const isAnnotation = id && id.includes("annotation");
  if (id && subItem?.type === "glyph" && !isAnnotation) {
    annotationsState[id].hidden = false;
    applyAnnotationStateUpdateToChart();
    chart.setProperties({ id, g: [] });
    return;
  }

  if (isAnnotation && subItem?.type === "glyph" && subItem.index === 1) {
    const subjectId = chart.getItem(id).subject;
    annotationsState[subjectId].hidden = true;
    removeAnnotationFromChart(id);
    chart.setProperties({
      id: subjectId,
      g: [annotationsState[subjectId].glyphStyling],
    });
    return;
  }

  if (isAnnotation) {
    // annotations are not selectable
    return;
  }

  clearTableStyling();
  // Update the selected node with the one passed in
  if (id === null) {
    const prevSelectedNodeId = selectedNodeId;
    selectedNodeId = null;
    resetButton.disabled = true;
    setNodeStyling("square");

    // click on chart background or network button - reshow hidden items
    showNodes = null;
    if (prevSelectedNodeId) {
      const result = await applyFilters();
      // Only layout if any nodes were shown or hidden
      if (result.shown.nodes.length > 0 || result.hidden.nodes.length > 0) {
        doLayout();
      }
    }
  } else {
    selectedNodeId = id;
    const item = chart.getItem(id);
    chart.selection(id);
    if (item && item.type === "node") {
      resetButton.disabled = false;
      const alertTableRow = document.getElementById(id);
      if (alertTableRow) {
        alertTableRow.classList.add("selected-row");
      }

      showNodes = {
        ...graph.distances(id, { direction: "to" }),
        ...graph.distances(id, { direction: "from" }),
      };
      if (item.d.dependencies.length) {
        item.d.dependencies.forEach((id) => (showNodes[id] = 1));
      }
      await applyFilters();
      setNodeStyling("rectangle");
      doLayout();
    }
  }
}

function storeNodeStyling() {
  const fontColour = bgColour;
  const backgroundColour = "rgba(0,0,0,0)";
  // set and store label styling for square and rectanglular nodes
  data.items.forEach((item) => {
    if (item.type === "node") {
      squareNodeText[item.id] = {
        t: [
          {
            fi: {
              t: fontIcons[item.d.type],
            },
            position: {
              vertical: "top",
            },
            fc: fontColour,
            fbc: backgroundColour,
          },
          {
            t: item.t.replace("-", "\n"),
            position: {
              vertical: "middle",
            },
            fc: fontColour,
            fbc: backgroundColour,
            fb: true,
          },
        ],
      };
      rectangleNodeText[item.id] = {
        fi: null,
        t: [
          {
            fi: {
              t: fontIcons[item.d.type],
            },
            position: {
              vertical: "middle",
              horizontal: "left",
            },
            fc: fontColour,
            fbc: backgroundColour,
            fs: 30,
            padding: {
              left: 13,
            },
          },
          {
            t: item.t,
            fc: fontColour,
            fbc: backgroundColour,
            position: {
              vertical: "top",
              horizontal: 45,
            },
            padding: {
              top: item.d.logMessage ? 19 : 24,
            },
            fb: true,
          },
          {
            fc: fontColour,
            fbc: backgroundColour,
            t: item.d.logMessage,
            fs: 9,
          },
        ],
      };
      initialNodeStyles.push({ id: item.id, c: item.c });
    } else if (item.type === "link") {
      initialNodeStyles.push({ id: item.id, c: item.c, c2: item.c2 });
    }
  });
}

function setNodeStyling(shape) {
  const isRectangle = shape === "rectangle";
  const styles = [];

  chart.each({ type: "node" }, (node) => {
    const id = node.id;
    const logLevel = node.d.logLevel;
    const b = id === selectedNodeId ? "white" : node.c;
    const bw = 6;
    let scaleFactor = 1;
    let w = 40;
    let h = 50;
    let borderRadius = 10;
    let t;
    let g = [];

    const dependencies = Object.keys(graph.distances(id, { direction: "from" }))
      .length;

    if (impactAlertsSelection === "impact" || logLevel) {
      if (dependencies > 0) {
        scaleFactor = getScaleFactor(dependencies);
      }
    }

    if (isRectangle) {
      t = [...rectangleNodeText[node.id].t];
      w = 300;
      h = 60;

      t[0].fs = 48;
      t[0].padding = { right: 30, left: 15 };
      t[1].margin = { left: 14 };
      if (t[2]) {
        t[2].margin = { left: 14 };
      }
    } else {
      t = [...squareNodeText[node.id].t];
      w = h *= scaleFactor;
      borderRadius *= scaleFactor;
      t[0].fs = 22 * scaleFactor;
      t[0].padding = { top: 7 * scaleFactor };
      t[1].fs = 5 * scaleFactor;
      t[1].padding = { top: 17 * scaleFactor };
    }

    if (impactAlertsSelection === "alerts" && logLevel) {
      const icon = fontIcons.exclamationTriangle;
      const iconSize = isRectangle ? 2 : 1.2 * scaleFactor;
      const glyphStyling = {
        c: "white",
        p: "ne",
        fi: {
          t: icon,
          c: bgColour,
        },
        e: iconSize,
        border: {
          width: 1.5,
          colour: bgColour,
        },
      };

      if (annotationsState[id].hidden) {
        g.push(glyphStyling);
      }
      annotationsState[id].glyphStyling = glyphStyling;
    }

    styles.push({ id, t, w, h, b, bw, borderRadius, g });
  });
  chart.setProperties(styles);
}

function initialiseInteractions() {
  // On hover, foreground connected nodes
  chart.on("hover", hoverReaction);
  // On click, focus on the selected node
  chart.on("click", setSelection);
  // Hide/show alerts list based on radio selection
  impactAlertsRadios.forEach((radio) => {
    radio.addEventListener("click", () => {
      showHideAlertsList();
      const isAnnotation =
        selectedNodeId && selectedNodeId.includes("annotation");
      setSelection({ id: isAnnotation ? null : selectedNodeId });
      doLayout();
    });
  });
  resetButton.addEventListener("click", () => setSelection({ id: null }));
}

function initialiseTableData() {
  data.items.forEach((item) => {
    if (item.type == "node" && item.d.logLevel) {
      alertsItems.push({
        label: item.t,
        type: item.d.logLevel,
        description: item.d.logMessage,
        nodeId: item.id,
      });
    }
  });
}

async function startKeyLines() {
  const imageAlignment = {
    "fas fa-server": { e: 0.75 },
    "fas fa-desktop": { e: 0.75 },
    "fas fa-exclamation-triangle": { dy: -11 },
  };

  const options = {
    arrows: "large",
    backColour: bgColour,
    navigation: false,
    overview: false,
    drag: {
      links: false,
    },
    hover: 0,
    handMode: true,
    minZoom: 0.02,
    iconFontFamily: "Font Awesome 5 Free",
    imageAlignment,
    // Disable chart selection styles
    selectedNode: {},
    selectedLink: {},
  };

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

  chart.load(data);
  initialiseInteractions();
  initialiseTableData();
  constructTable();
  storeNodeStyling();
  setNodeStyling("square");
  doLayout();
}

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

window.addEventListener("DOMContentLoaded", loadKeyLines);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
  <head>
    <meta charset="UTF-8">
    <title>Impact Analysis</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="/impact.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="/impact.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%;">Impact Analysis</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>
                  Hover over a node to explore its dependencies and 
                  select it to change the view to a hierarchy of its dependants. 
                </p>
                <p>Click on the background or the <strong>Reset View</strong> button to return to the network view.
                  
                </p>
                <input class="btn btn-block" type="button" value="Reset View" disabled id="reset">
                <p>
                  Toggle between impact analysis and the alerts list views.
                  
                </p>
                <fieldset>   
                  <div>
                    <legend>Impact / Alerts</legend>
                    <label class="radio" style="display: inline-block">
                      <input type="radio" name="dependencyView" value="impact" checked="checked"> Impact Analysis
                    </label>
                    <label class="radio" style="display: inline-block">
                      <input type="radio" name="dependencyView" value="alerts"> Alerts List
                    </label>
                  </div>
                </fieldset>
                <fieldset class="hidden" id="alerts">
                  <p>Click the &nbsp;<i class="fas fa-exclamation-triangle" style="background-color: #ffffff; border-radius: 30px; border: solid 3px #ffffff; outline: solid 2px #58595b; color: #212121; font-size: smaller"></i>&nbsp;&nbsp;alert glyph
                    to open the alert description. You can collapse it again by clicking the&nbsp;&nbsp;<i class="fas fa-eye-slash" style="background-color: #ffffff; border-radius: 30px; border: solid 3px #ffffff; outline: solid 2px #58595b; color: #212121; font-size: smaller;"></i>&nbsp;&nbsp;hide button.
                    
                  </p>
                  <legend>List of Alerts</legend>
                  <p>Select an alert to view it in the chart.</p>
                  <table>
                    <thead>
                      <tr>
                        <th>ID</th>
                        <th>Type</th>
                        <th>Description</th>
                      </tr>
                    </thead>
                    <tbody class="alertsTableBody"></tbody>
                  </table>
                </fieldset>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
    <div id="moreParent">
      <div id="moreContainer">
      </div>
    </div>
    <div id="lazyScripts">
    </div>
  </body>
</html>

#nodeDependantWarning {
  background-color: #fcf8e3;
  padding-left: 4px;
  border-style: solid;
  border-width: 1px;
  border-radius: 2px;
}

.radio {
  padding-left: 25px;
}

table {
  border: 1px solid #969696;
  border-collapse: collapse;
  font-size: 14px;
  width: 100%;
  height: 30vh;
}

table thead:hover {
  background-color: #969696;
}

table thead tr {
  display: block;
  width: 100%;
  background-color: #969696;
}

table thead th {
  color: #212121;
  background-color: #969696;
  position: sticky;
  top: 0;
  text-align: left;
}

table tbody {
  display: block;
  overflow-y: scroll;
  height: 100%;
  background-color: white;
}

table tbody::-webkit-scrollbar {
  width: 8px;
}

table tbody::-webkit-scrollbar-thumb {
  background: #969696;
  border-radius: 5px;
}

table tbody tr {
  border-radius: 30px;
  cursor: pointer;
  display: table-row;
  transition: all 0.3s ease;
}

.selected-row, #alerts .selected-row:hover {
  background-color: #212121;
  color: white;
}

table tr *:nth-child(1) {
  width: 130px;
}

table tr *:nth-child(2) {
  text-align: center;
  width: 100px;
}

table tr *:nth-child(3) {
  width: 170px;
}

table th,
table td {
  padding: 0.5em;
}

#reset {
  text-align: center;
  margin: 12px 0px;
  border-style: solid;
  border-width: 1px;
}
Loading source