//
// 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
