//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//
//     Sample Code
//!    Reveal trends and patterns from malware tracker data.
import KeyLines from "keylines";
import data from "./malware-data.js";

let chart;
let timebar;
const IPsLinkedHosts = {};
const maxMalwareSelection = 3;
const tooltipElements = {
  tooltip: document.getElementById("tooltip"),
  servers: document.getElementById("servers"),
};

const onlineOnlyButton = document.getElementById("onlineOnly");

async function doLayout() {
  await chart.layout("organic", {
    time: 400,
    easing: "linear",
    mode: "adaptive",
  });
}

function getSelectionCriteria(malwares) {
  const showOnlyOnline = onlineOnlyButton.value === "Show All";
  return (item) => {
    const status = showOnlyOnline ? item.d.status === "online" : true;

    if (item.d.type === "asn") return true;

    if (malwares) return item.d.malware in malwares && status;
    return status;
  };
}

function highlightMalware(malwares) {
  timebar.selection(Object.values(malwares));
  chart.foreground(
    getSelectionCriteria(Object.keys(malwares).length !== 0 ? malwares : null),
    { animate: false, type: "link" }
  );
}

function showTrends() {
  // Find the checked checkboxes
  const checkedMalwares = Array.from(
    document.querySelectorAll("input[name=malware]:checked")
  );

  // If there are more than 3, uncheck the 4th one until there are just 3
  while (checkedMalwares.length > maxMalwareSelection) {
    checkedMalwares.pop().checked = false;
  }

  const reachedMaxSelections = checkedMalwares.length === maxMalwareSelection;
  document
    .querySelectorAll("input[name=malware]:not(:checked)")
    .forEach((radioButton) => {
      radioButton.disabled = reachedMaxSelections;
      radioButton.parentElement.style.color = reachedMaxSelections
        ? "DarkGray"
        : "black";
    });

  const malwareNames = {};

  checkedMalwares.forEach((element, index) => {
    const elementColour = element.parentElement
      .getElementsByTagName("dt")
      .item(0).style.background;
    malwareNames[element.value] = { id: [], index, c: elementColour };
  });

  chart.each({ type: "link" }, (link) => {
    if (link.d.malware in malwareNames) {
      malwareNames[link.d.malware].id.push(link.id1);
    }
  });

  // Update timebar selection and chart foregrounding
  timebar.selection([]);
  highlightMalware(malwareNames);
}

function showInfo() {
  const info = document.getElementById("info");

  const selection = chart.selection();

  let title = "";
  let status = "";
  let imageDisplayStyle = "none";

  if (selection.length === 1) {
    const item = chart.getItem(selection[0]);

    if (item.type === "node" && item.d.type === "host") {
      title = item.d.host;
      status = `Current status: <span class="${item.d.status}">${item.d.status}</span>`;
      imageDisplayStyle = "block";
    }
  }
  info.getElementsByTagName("h5").item(0).innerText = title;
  info.getElementsByTagName("p").item(0).innerHTML = status;
  info.getElementsByTagName("img").item(0).style.display = imageDisplayStyle;
}

function mergeObjects(object2, object1) {
  Object.keys(object1).forEach((host) => {
    object2[host] = object1[host];
  });
  return object2;
}

// Cache results for filtering by time. Only host nodes have timestamps so we need
// a reference for other node types to see what they're linked to
function calculateIPsNeighbours() {
  // Build a quick lookup for each item using IP & ISP
  chart.each({ type: "link" }, (link) => {
    if (link.d.type === "Host2IP") {
      IPsLinkedHosts[link.id2] = IPsLinkedHosts[link.id2] || {};
      IPsLinkedHosts[link.id2][link.id1] = 1;
    }
  });

  // Now do another pass to filter for the ISP
  chart.each({ type: "link" }, (link) => {
    if (link.d.type === "IP2ISP") {
      IPsLinkedHosts[link.id2] = IPsLinkedHosts[link.id2] || {};
      // now add all the hosts associated with the IP
      IPsLinkedHosts[link.id2] = mergeObjects(
        IPsLinkedHosts[link.id2],
        IPsLinkedHosts[link.id1]
      );
    }
  });
}

function checkNeighbours(obj) {
  return obj && Object.keys(obj).some((id) => timebar.inRange(id));
}

function filterByTimeRange(item) {
  if (item.type === "node") {
    return timebar.inRange(item.id) || checkNeighbours(IPsLinkedHosts[item.id]);
  }
  return (
    timebar.inRange(item.id1) ||
    timebar.inRange(item.id2) ||
    checkNeighbours(IPsLinkedHosts[item.id1]) ||
    checkNeighbours(IPsLinkedHosts[item.id2])
  );
}

function hideTooltip() {
  tooltipElements.tooltip.classList.add("fadeout");
}

async function filterItemsInTimeRange() {
  hideTooltip();
  // Filter the chart to show only items in the new range
  await chart.filter(filterByTimeRange, {
    animate: false,
    type: "all",
    hideSingletons: true,
  });
  // And then adjust the chart's layout
  doLayout();
}

function showTooltip({ type, value: numberOfServer, tooltipX, tooltipY }) {
  const hoverOverData = type === "bar" || type === "selection";
  const { servers, tooltip } = tooltipElements;
  if (hoverOverData) {
    // If the target is data, update the tooltip content
    servers.innerText = numberOfServer;

    // The top needs to be adjusted to accommodate the height of the tooltip
    const tooltipTop = tooltipY - tooltip.offsetHeight - 15;
    // Shift left by half width to centre it
    const tooltipLeft = tooltipX - tooltip.offsetWidth / 2;
    // Set the position of the tooltip
    tooltip.style.left = `${tooltipLeft}px`;
    tooltip.style.top = `${tooltipTop}px`;
    // Show the tooltip
    tooltip.classList.remove("fadeout");
  } else {
    hideTooltip();
  }
}

function initialiseInteractions() {
  timebar.on("change", filterItemsInTimeRange);
  timebar.on("hover", showTooltip);
  chart.on("selection-change", showInfo);

  const malwareCheckboxes = document.getElementsByClassName("malware");

  Array.from(malwareCheckboxes).forEach((checkbox) => {
    checkbox.addEventListener("change", showTrends);
  });

  document.getElementById("clearFilter").addEventListener("click", () => {
    Array.from(malwareCheckboxes).forEach((checkbox) => {
      checkbox.checked = false;
    });
    showTrends();
    doLayout();
  });

  onlineOnlyButton.addEventListener("click", () => {
    onlineOnlyButton.value =
      onlineOnlyButton.value === "Show All" ? "Only Show Online" : "Show All";
    showTrends();
  });
}

async function loadDataAndLayout() {
  chart.load(data);
  await chart.zoom("fit");
  await timebar.load(data);
  timebar.zoom("fit", { animate: false });
  // Run the layout
  doLayout();
}

async function loadKeyLines() {
  const chartOptions = {
    logo: "/images/Logo.png",
    minZoom: 0.01,
    handMode: true,
    linkEnds: { avoidLabels: false },
    overview: {
      icon: false,
      shown: false,
    },
  };

  const timebarOptions = {
    showPlay: false,
    showExtend: true,
    maxRange: {
      value: 5,
      units: "year",
    },
  };

  const components = await KeyLines.create([
    { id: "kl", container: "klchart", options: chartOptions },
    {
      id: "tl",
      container: "kltimebar",
      type: "timebar",
      options: timebarOptions,
    },
  ]);

  chart = components[0];
  timebar = components[1];

  await loadDataAndLayout();
  calculateIPsNeighbours();
  initialiseInteractions();
}

window.addEventListener("DOMContentLoaded", loadKeyLines);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
  <head>
    <meta charset="UTF-8">
    <title>Find Malware Trends</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="/malware.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="/malware.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 klchart-timebar" id="klchart">
          </div>
          <div class="kltimebar" id="kltimebar">
            <div class="tooltip fadeout" id="tooltip" style="position: absolute; pointer-events: none;">
              <div class="arrow"></div>
              <div class="inner">
                <p>Servers Detected</p><span id="servers"></span>
              </div>
            </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%;">Find Malware Trends</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">
                <fieldset>
                  <p>
                    Explore patterns of malware propagation by visualising the relation between C&amp;C servers
                    and the hosts and ISPs they are linked to.
                  </p>
                </fieldset>
                <fieldset>
                  <legend>Malware Trends</legend>
                  <p>Select up to three malware families to see their distribution over time.</p>
                  <div class="row-fluid">
                    <dl class="span6" style="margin-bottom: 5px; margin-top: 0px;">
                      <label class="checkbox zeus">
                        <input class="malware" type="checkbox" value="ZeuS" name="malware">
                        <dt style="background: #e41a1c">&nbsp;</dt>
                        <dd>ZeuS</dd>
                      </label>
                      <label class="checkbox iceix">
                        <input class="malware" type="checkbox" value="Ice IX" name="malware">
                        <dt style="background: #377eb8">&nbsp;</dt>
                        <dd>IceIX</dd>
                      </label>
                      <label class="checkbox vmzeus">
                        <input class="malware" type="checkbox" value="VMZeuS" name="malware">
                        <dt style="background: #4daf4a">&nbsp;</dt>
                        <dd>VMZeuS</dd>
                      </label>
                      <label class="checkbox citadel">
                        <input class="malware" type="checkbox" value="Citadel" name="malware">
                        <dt style="background: #984ea3">&nbsp;</dt>
                        <dd>Citadel</dd>
                      </label>
                    </dl>
                    <dl class="span6" style="margin: 0px 0px 5px 0px;">
                      <label class="checkbox kins">
                        <input class="malware" type="checkbox" value="KINS" name="malware">
                        <dt style="background: #ff7f00">&nbsp;</dt>
                        <dd>KINS</dd>
                      </label>
                      <label class="checkbox palevo">
                        <input class="malware" type="checkbox" value="Palevo" name="malware">
                        <dt style="background: #ffeb3b">&nbsp;</dt>
                        <dd>Palevo</dd>
                      </label>
                      <label class="checkbox feodoc">
                        <input class="malware" type="checkbox" value="Feodo Version C" name="malware">
                        <dt style="background: #756bb1">&nbsp;</dt>
                        <dd>Feodo Version C</dd>
                      </label>
                      <label class="checkbox feodod">
                        <input class="malware" type="checkbox" value="Feodo Version D" name="malware">
                        <dt style="background: #54278f">&nbsp;</dt>
                        <dd>Feodo Version D</dd>
                      </label>
                    </dl>
                  </div>
                  <div class="row-fluid">
                    <input class="btn" type="button" value="Clear" id="clearFilter">
                    <input class="btn pull-right" type="button" value="Only Show Online" id="onlineOnly">
                  </div>
                </fieldset>
                <fieldset style="min-height: 110px;">
                  <p style="margin-top: 10px;">Select a host <img src="/images/icons/email.png" style="width: 22px; height: 22px;"> to inspect</p>
                  <div class="row-fluid" id="info">
                    <div class="imagediv"><img src="/images/icons/email.png" style="width: 64px; height: 64px; display: none;"></div>
                    <div class="textdiv">
                      <h5></h5>
                      <p></p>
                    </div>
                  </div>
                </fieldset>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
    <div id="moreParent">
      <div id="moreContainer">
      </div>
    </div>
    <div id="lazyScripts">
    </div>
  </body>
</html>
dl {
  margin-bottom:50px;
}
 
dl dt {
  color:#fff;
  float:left; 
  font-weight:bold; 
  margin-right:10px; 
  padding:3px;
  width: 18px;
  border-radius: 18px;
  line-height: 12px;
}
 
dl dd {
  margin:2px 0; 
  padding:5px 0;
  line-height: 12px;
}

span.unknown, span.online, span.offline {
  font-weight: bold;
}

.unknown {
  color: grey;  
}

.online {
  color: green;
}

.offline {
  color: red;
}

#loadingBarText {
  text-align: center;
  margin-top: -40px;
}

#info {
  height: 70px;
  max-width: 99%;
  border: lightgray solid 1px;
}

/* Tooltip styling */

#tooltip {
  opacity: 0.8;
  transition: opacity 0.3s ease;
  display: block;
  line-height: 18px;
  text-align: center;
  z-index: 1001;
}

#tooltip .inner {
  background-color: grey;
  color: white;
  padding: 4px;
}

#tooltip .arrow {
  position: absolute;
  border: solid 8px grey;
  border-top-color: transparent;
  border-left-color: transparent;
  transform: translate(-5px, 60px) rotateZ(45deg);
  position: absolute;
  top: -10px;
  left: 48px;
  width: 16px;
  height: 16px;
}

#tooltip.fadeout {
  opacity: 0;
}

#servers {
  font-size: 22px;
}

#info {
  display: inline-block;
  width: 100%;
}
#info .imagediv {
  display: inline-block;
  width: 64px;
}
#info .textdiv {
  position: relative;
  top: -10px;
  display: inline-block;
  width: calc(100% - 68px);
}
Loading source