//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
//! Discover new ways to visualise large networks.
import KeyLines from "keylines";
let chart;
let dataSizeOverlay;
function getById(id) {
return document.getElementById(id);
}
const progressBarContainer = getById("progressBarContainer");
const progressBar = getById("progressBar");
const progressFrame = getById("progressFrame");
const layoutButton = getById("layoutButton");
const fpsLabel = getById("fps");
function updateProgressBar(progress) {
const percentage = (progress * 100).toFixed(2);
progressBar.style.width = `${percentage}%`;
}
async function doLayout() {
layoutButton.disabled = true;
progressBarContainer.style.display = "block";
await chart.layout("organic", { time: 1500 });
progressBarContainer.style.display = "none";
layoutButton.disabled = false;
}
function setupLayoutUX() {
// Add progress event listener
chart.on("progress", ({ progress, task }) => {
if (task === "layout") {
updateProgressBar(progress);
}
});
layoutButton.addEventListener("click", () => doLayout());
}
function fpsCounter(blend = 0.33) {
let tick = Date.now();
let mean;
(function next() {
requestAnimationFrame((tock) => {
const dt = tock - tick;
mean = mean ? mean * blend + dt * (1 - blend) : dt;
tick = tock;
next();
});
})();
return { get: () => Math.round(1000 / mean) };
}
function toggleSwitch(name, onOn, onOff) {
const onElement = getById(`${name}On`);
const offElement = getById(`${name}Off`);
onElement.addEventListener("click", (evt) => {
onElement.classList.add("active", "btn-kl");
offElement.classList.remove("active", "btn-kl");
offElement.disabled = false;
onElement.disabled = true;
onOn();
evt.preventDefault();
});
offElement.addEventListener("click", (evt) => {
onElement.classList.remove("active", "btn-kl");
onElement.disabled = false;
offElement.disabled = true;
offElement.classList.add("active", "btn-kl");
onOff();
evt.preventDefault();
});
}
async function loadAndStart() {
dataSizeOverlay = document.getElementsByClassName("datasize")[0];
const chartDefinition = {
container: "klchart",
options: {
backColour: "#2d383f",
controlTheme: "dark",
drag: {
links: false,
},
minZoom: 0.01,
overview: {
backColour: "#2d383f",
borderColour: "grey",
},
handMode: true,
},
};
chart = await KeyLines.create(chartDefinition);
// display frame rate
setInterval(
(fps) => {
fpsLabel.innerText = fps.get() + " Frames per Second";
},
400,
fpsCounter()
);
function adaptiveStylingOn() {
chart.options({ zoom: { adaptiveStyling: true } });
}
function adaptiveStylingOff() {
chart.options({ zoom: { adaptiveStyling: false } });
}
function darkModeOn() {
chart.options({
backColour: "#2d383f",
controlTheme: "dark",
overview: {
backColour: "#2d383f",
borderColour: "grey",
},
});
progressFrame.style["border-color"] = "white";
dataSizeOverlay.classList.add("dark");
}
function darkModeOff() {
chart.options({
backColour: "white",
controlTheme: "light",
overview: {
backColour: "white",
borderColour: "rgb(243, 246, 247)",
},
});
progressFrame.style["border-color"] = "black";
dataSizeOverlay.classList.remove("dark");
}
function gradientLinksOn() {
const links = [];
chart.each({ type: "link" }, (link) => {
links.push({ id: link.id, c: link.d.c, c2: link.d.c2 });
});
chart.setProperties(links);
}
function gradientLinksOff() {
const links = [];
chart.each({ type: "link" }, (link) => {
links.push({
id: link.id,
c: null,
c2: null,
d: { c: link.c, c2: link.c2 },
});
});
chart.setProperties(links);
}
toggleSwitch("adaptiveStyling", adaptiveStylingOn, adaptiveStylingOff);
toggleSwitch("darkMode", darkModeOn, darkModeOff);
toggleSwitch("gradientLinks", gradientLinksOn, gradientLinksOff);
const data = await (await fetch("/worldflights-data.json")).json();
chart.load(data);
setupLayoutUX();
await doLayout();
}
window.addEventListener("DOMContentLoaded", loadAndStart);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
<head>
<meta charset="UTF-8">
<title>Visualising Big Data</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="/worldflights.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="/worldflights.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 class="klchart" id="klchart">
<div id="progressBarContainer">
<div class="progress" id="progressFrame">
<div id="progressBar"></div>
</div>
</div>
<div class="datasize dark" id="dataSize">
<h5 id="fps">0 Frames per Second</h5>
<p>Nodes: 2875</p>
<p>Links: 13139</p>
</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%;">Visualising Big Data</text>
</svg>
</div>
<div class="tab-content-panel" data-tab-group="rhs">
<div class="toggle-content is-visible tabcontent" id="controlsTab">
<p>Discover new ways to visualise large amounts of data.</p>
<form autocomplete="off" onsubmit="return false" id="rhsForm">
<div class="cicontent">
<p>Zoom in and out to trigger adaptive styling and watch the chart maintain a clean view with the right amount of detail.</p>
<input class="btn" type="button" value="Layout" style="text-align: center; width: 200px; margin-top:12px;" disabled id="layoutButton">
<fieldset>
<legend>Options</legend>
<h4>Adaptive Styling</h4>
<div style="margin-left:14px; margin-bottom: 16px;">
<div class="btn-group">
<input class="btn btn-kl active" type="button" value="On" disabled id="adaptiveStylingOn">
<input class="btn" type="button" value="Off" id="adaptiveStylingOff">
</div>
</div>
<h4>Dark Mode</h4>
<div style="margin-left:14px; margin-bottom: 16px;">
<div class="btn-group">
<input class="btn btn-kl active" type="button" value="On" disabled id="darkModeOn">
<input class="btn" type="button" value="Off" id="darkModeOff">
</div>
</div>
<h4>Gradient Links</h4>
<div style="margin-left:14px; margin-bottom: 16px;">
<div class="btn-group">
<input class="btn btn-kl active" type="button" value="On" disabled id="gradientLinksOn">
<input class="btn" type="button" value="Off" id="gradientLinksOff">
</div>
</div>
</fieldset>
</div>
</form>
</div>
</div>
</div>
</div>
<div id="moreParent">
<div id="moreContainer">
</div>
</div>
<div id="lazyScripts">
</div>
</body>
</html>
.klchart {
background-color: #2d383f;
}
.datasize {
position: absolute;
bottom: 0px;
height: 100px;
line-height:16px;
left: 16px;
color: #2d383f;
background-color: transparent;
}
.datasize > h5 {
font-size: 20px;
}
.datasize.dark {
color: #eee;
}
.progress {
position: absolute;
top: calc(50% - 22.5px);
left: 30%;
width: 40%;
height: 45px;
padding: 4px;
opacity: 0.9;
border: white 3px solid;
}
.progress > div {
background-color: #009968;
width: 0%;
height: 100%;
transition: width 1s ease-in;
}
Loading source
