//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
// Sample Code
//
//! Explore the source and distribution of ransomware attacks over time.
import KeyLines from "keylines";
import { data, comboData, ransomwareColours } from "./ransomware-data.js";
let chart;
let timebar;
// Scale the closed combo sizes at slightly less than the square root of their
// node count so that the larger combos (e.g. USA) don't dominate too much
// and the smaller combos aren't too tiny
const scalePower = 0.4;
function isCombo(ids) {
return chart.combo().isCombo(ids, { type: "node" });
}
function showRansomware(item) {
const checked = [
...document.querySelectorAll('input[name="ransomware"]:checked'),
];
return checked.some((c) => item.d.ransomware === c.id);
}
function enableButton(type, enable) {
document.getElementById(`${type}Run`).disabled = !enable;
}
function layout(mode = "full") {
const layoutType = document.getElementById("layoutType").value;
const opts = layoutType === "organic" ? { mode } : {};
chart.layout(layoutType, opts);
}
function timebarFilterCriteria(item) {
// If the ransomware is not checked
if (item.d.ransomware && !showRansomware(item)) {
return false;
}
if (item.d.type === "ip") {
const hosts = item.d.host;
return hosts.some((host) => timebar.inRange(host));
}
if (item.d.type === "host") {
return timebar.inRange(item.id);
}
return false; // Other cases should return false
}
async function timebarChange() {
// filter the chart to show only items in the new range
const changes = await chart.filter(timebarFilterCriteria, {
type: "node",
animate: false,
hideSingletons: true,
});
// update the size of the combo based on the changes object returned by the filter
const visualProperties = changes.combos.nodes.map((comboInfo) => ({
id: comboInfo.id,
e: comboInfo.nodes.length ** scalePower,
}));
// When playing the timebar sometimes it can be helpful to overwrite previous animations
// => queue: false
await chart.animateProperties(visualProperties, { time: 300, queue: false });
}
function isOnlyCombos(items) {
return items.length && items.every((item) => isCombo(item.id));
}
function isOnlyTypeSelected(type, items) {
return items.every((item) => item.d.type === type);
}
function groupByRansomware(items) {
const result = {};
items.forEach((item) => {
const group = item.d.ransomware;
if (result[group]) {
result[group].push(item);
} else {
result[group] = [item];
}
});
return result;
}
function ransomwareSelection(nodes, countrySelected, subsetHost) {
return Object.entries(groupByRansomware(nodes)).map(
([name, group], index) => {
let ids;
const firstItem = group[0];
if (isCombo(firstItem.id)) {
if (countrySelected) {
ids = firstItem.d.lookup[countrySelected].filter((id) =>
subsetHost ? subsetHost.includes(id) : true
);
} else {
// Selected a ransomware
ids = [];
Object.values(firstItem.d.lookup).forEach((i) => {
ids = ids.concat(i);
});
}
} else {
ids = group.map((item) => item.id);
}
return { id: ids, index, c: ransomwareColours[name] };
}
);
}
function chartSelectionChange() {
// Reset the previous timebar selection
timebar.selection([]);
// Get only the nodes within the selection
const selection = chart.getItem(chart.selection());
// Enable or disable the uncombine button
enableButton("uncombine", isOnlyCombos(selection));
let tbSelection = []; // The new selection of the timebar
// show the trend of the ransomwares for the country combo and ip nodes
if (isOnlyTypeSelected("ip", selection)) {
const neighbours = {};
const hosts = {};
// For each country
selection.forEach((ip) => {
neighbours[ip.d.country] = neighbours[ip.d.country] || [];
neighbours[ip.d.country] = neighbours[ip.d.country].concat(
chart.graph().neighbours(ip.id).nodes
);
if (ip.d.host) {
hosts[ip.d.country] = hosts[ip.d.country] || [];
hosts[ip.d.country] = hosts[ip.d.country].concat(ip.d.host);
}
});
// For every neighbour to selected countries
Object.entries(neighbours).forEach(([country, nodes]) => {
// Get the timebar selection
const newSelection = ransomwareSelection(
chart.getItem(nodes),
country,
hosts[country]
);
tbSelection = tbSelection.concat(newSelection);
});
}
// Show the trend for the ransomware combos and host nodes
if (isOnlyTypeSelected("host", selection)) {
tbSelection = ransomwareSelection(selection);
}
timebar.selection(tbSelection); // Set the new selection to the timebar
}
function selectionChangeAndLayout() {
chartSelectionChange();
layout("adaptive");
}
async function filterRansomware() {
await timebarChange();
selectionChangeAndLayout();
}
function uncombineAction() {
chart.combo().uncombine(chart.selection(), { animate: false, select: false });
// run the filter of ransomware
filterRansomware();
// Enable the combine button
enableButton("combine", true);
}
async function combineAction() {
// Find combos that have been uncombined
// Filter out ones that are still combos as otherwise, Keylines raises an error
const toBeCombined = comboData.filter(
({ ids }) => chart.combo().find(ids[0]) === null
);
await chart
.combo()
.combine(toBeCombined, {
select: false,
animate: false,
arrange: "concentric",
});
// run the filter of ransomware
filterRansomware();
// Disable the combine button
enableButton("combine", false);
}
function initialiseEvents() {
chart.on("selection-change", chartSelectionChange); // Align the chart selection to the timebar
timebar.on("change", timebarChange); // Filter the nodes to match the timebar range
document
.getElementById("layoutRun")
.addEventListener("click", () => layout()); // Run the layout
document
.getElementById("layoutType")
.addEventListener("change", () => layout()); // Run the layout when choosing a layout
document
.getElementById("combineRun")
.addEventListener("click", combineAction); // Combine all the nodes
document
.getElementById("uncombineRun")
.addEventListener("click", uncombineAction); // Uncombine selected nodes
document.querySelectorAll('input[name="ransomware"]').forEach((btn) => {
// Filter the ransomwares
btn.addEventListener("change", filterRansomware);
btn.addEventListener("keyup", filterRansomware);
});
}
async function startKeyLines() {
const chartOptions = {
logo: "/images/Logo.png",
hover: 100,
marqueeLinkSelection: "off",
imageAlignment: {
"/images/icons/virus.svg": { e: 0.9 },
},
selectedNode: {
fb: true,
ha0: {
c: "#555",
r: "31",
w: "3",
},
oc: { bw: 20 },
},
iconFontFamily: "Font Awesome 5 Free",
handMode: true,
};
[chart, timebar] = await KeyLines.create([
{
container: "klchart",
type: "chart",
options: chartOptions,
},
{
container: "kltimebar",
type: "timebar",
options: { showExtend: true },
},
]);
chart.load(data);
timebar.load(data);
// Fit the initial view to the loaded data
chart.zoom("fit", { animate: false });
timebar.zoom("fit", { animate: false });
combineAction();
initialiseEvents();
}
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>Ransomware Attacks</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="/ransomware.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="/ransomware.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 klchart-timebar" id="klchart">
</div>
<div class="kltimebar" id="kltimebar">
</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%;">Ransomware Attacks</text>
</svg>
</div>
<div class="tab-content-panel" data-tab-group="rhs">
<div class="toggle-content is-visible tabcontent" id="controlsTab">
<p>Explore the source and distribution of ransomware attacks over time:</p>
<form autocomplete="off" onsubmit="return false" id="rhsForm">
<div class="cicontent">
<ul>
<li>select a ransomware family or country to show time-based activity</li>
<li>double-click ransomware families or countries to explore malware hosts</li>
<li>use time bar controls to filter the chart</li>
<li>choose the best layout to display filtered data</li>
</ul><br>
<div class="form-inline">
<select class="inline input-medium" id="layoutType">
<option value="organic">Organic</option>
<option value="structural">Structural</option>
</select>
<input class="btn" type="button" value="Layout" id="layoutRun">
</div><br>
<p>Filter by ransomware family:<br>
<label class="checkbox inline">
<input type="checkbox" id="TeslaCrypt" name="ransomware" checked="checked"><span class="label" style="background: #FF5964">TeslaCrypt</span>
</label>
<label class="checkbox inline">
<input type="checkbox" id="CryptoWall" name="ransomware" checked="checked"><span class="label" style="background: #00BFFF">CryptoWall</span>
</label>
<label class="checkbox inline">
<input type="checkbox" id="Locky" name="ransomware" checked="checked"><span class="label" style="background: #A5DF00">Locky</span>
</label>
</p><br>
<p>Uncombine selected combos, or combine all.</p>
<input class="btn" type="button" value="Uncombine" disabled="disabled" id="uncombineRun">
<input class="btn" type="button" value="Combine All" disabled="disabled" id="combineRun">
</div>
</form>
</div>
</div>
</div>
</div>
<div id="moreParent">
<div id="moreContainer">
</div>
</div>
<div id="lazyScripts">
</div>
</body>
</html>
#layoutRun {
margin-top: 0;
}
.checkbox.inline {
vertical-align: middle;
}
.checkbox input[type='checkbox'] {
float: none;
margin-right: 4px;
}
.form-inline * {
margin: 0px;
vertical-align: middle;
}
Loading source
