Displaying large networks and amounts of data all at once is tricky, but KeyLines offers features that can make even large data visualisations clear and effective.
Organic layout
This demo uses organic layout to make sense of the data's complex structure. This, combined with the enhanced rendering performance you can achieve with WebGL, creates a responsive, clear and engaging chart.
Organic also supports incrementally reporting progress events, which provide information about the layout's progress. This can be used for example to display a progress indicator and improve the user's experience while the layout is running.
Adaptive styling
Adaptive styling enlarges small nodes so that you can see them better, and fades out links and text when they're too closely packed or too small to read. This means that details are shown when you need them, but don't clutter the chart when they're unnecessary.
The data is taken from OpenFlights, available under the Open Database License (ODbL).
Chart Options used:
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="@ci/theme/kl/css/keylines.css" />
<link rel="stylesheet" type="text/css" href="@ci/theme/kl/css/minimalsdk.css" />
<link rel="stylesheet" type="text/css" href="@ci/theme/kl/css/sdk-layout.css" />
<link rel="stylesheet" type="text/css" href="@ci/theme/kl/css/demo.css" />
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div id="klchart" class="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>
<script type="module" src="./code.js"></script>
</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,
.datasize.dark p {
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;
}