//
//
// 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 <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> alert glyph
to open the alert description. You can collapse it again by clicking the <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> 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
