Search

Network Alerts

Combos

Help users identify and understand alerts in your networks.

Network Alerts
View live example →

This fictional demo of a company IT network displays a number of offices at different locations and subnets within them, all speaking to a central server through which common services are accessed. Links represent communication between machines.

The data includes the machine's IP address, details about the device within the network and the ports through which inbound and outbound data flows.

Alerts

IT network visualisations often need to identify alerts on particular machines. Alerts might be raised manually or generated from detectable behaviours such as traffic to blacklisted addresses or downtime within the network. This chart displays two alerts and uses several techniques to highlight them:

  • Glyphs on nodes and links
  • Automatic pan and zoom to the area
  • Pings to focus attention after movement
  • Contrasting colours on important nodes and links

Structure

This demo uses open combos to provide a 'semantic zoom' over the network data. Using combos lets you browse the chart at different levels of detail. For example, it is as useful to know that there is a dangerous link between the Paris and Cambridge offices as it is to know there's insecure data travelling between 10.0.203.101 and 192.168.247.168.

When nodes are combined, links connecting nodes inside the combo to nodes outside it are also combined. This reduces clutter, but sometimes it's useful to see the links between nodes directly. This demo uses the chart.combo().reveal() function to always show links with an associated alert, and to show links from the currently selected node.

Key functions used:

import KeyLines from "keylines";
import { data, colours } from "./data.js";

let chart;
let graph;

// State tracking
let alwaysRevealedLinks = [];
const openCombos = {};
let visiblePorts = [];

// Map data values to strings for the Details panel
const osMap = {
  win: "Windows",
  mac: "macOS",
  linux: "Linux",
  phone: "Android",
};

const typeMap = {
  linux: "Computer",
  win: "Computer",
  mac: "Computer",
  printer: "Printer",
  phone: "Smartphone",
  server: "Server",
};

function getComboShape() {
  return document.querySelector('input[name="shape"]:checked').value;
}

function getComboArrangement() {
  const comboShape = getComboShape();
  const isCircle = comboShape === "circle";
  return isCircle ? "concentric" : "grid";
}

// 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 getComboList() {
  const comboIds = [];
  chart.each({ type: "node", items: "all" }, ({ id }) => {
    if (chart.combo().isCombo(id)) {
      comboIds.push(id);
    }
  });
  return comboIds;
}

function decorateAlertLinks(linkIds) {
  chart.setProperties(
    linkIds.map((id) => ({
      id,
      c: colours.alertRed,
      g: [
        {
          c: colours.alertRed,
          b: colours.glyphBorder,
          fi: { t: "fas fa-exclamation", c: colours.glyphFontColor },
          e: 1.5,
        },
      ],
    })),
  );
}

// Update the Details panel with details of a given item.
function updateUI(item) {
  // hide any details that are already shown
  document.querySelectorAll(".details").forEach((detail) => {
    detail.classList.add("hide");
  });

  let root;
  if (item) {
    const { type } = item.d;
    // For combo nodes, just display some very basic information
    if (chart.combo().isCombo(item.id)) {
      root = document.querySelector(".combo.details");

      // display the type of item
      root.querySelector(".field.combo-type label").textContent =
        type || "Connection";

      // and the label, if a node, or the labels of the nodes at either end if a link
      const content =
        item.type === "link"
          ? `${chart.getItem(item.id1).t[0].t} <--> ${chart.getItem(item.id2).t[0].t}`
          : item.t[0].t;
      root.querySelector(".field.detail .value").textContent = content;
    } else {
      // For regular nodes and links, display more detailed information (including alerts).
      root = document.querySelector(`.${item.type}.details`);

      if (item.type === "node") {
        root.querySelector(".field.ip .value").textContent = item.t[0].t;

        root.querySelector(".field.type .value").textContent = typeMap[type];

        const os = item.d.type;
        root.querySelector(".field.os .value").textContent = osMap[os] || "-";

        root.querySelector(".field.host .value").textContent = item.d.network;
      } else {
        const src = chart.getItem(item.id1);
        root.querySelector(".field.from .value").textContent =
          `${src.t[0].t}:${item.d.port1}`;

        const target = chart.getItem(item.id2);
        root.querySelector(".field.to .value").textContent =
          `${target.t[0].t}:${item.d.port2}`;
      }

      root.querySelector(".field.msg .value").innerHTML =
        `<li>${item.d.alert || "None"}</li>`;
    }
    root.classList.remove("hide");
  } else {
    document.querySelector(".about.details").classList.remove("hide");
  }
}

// Update foreground state according to what is selected
// If a node: it's the node, its neighbours and the links between
// If a link: it's the link and the nodes at the ends of the link
function updateForeground(id) {
  const toForeground = {};
  const item = chart.getItem(id);
  if (item) {
    toForeground[id] = true;
    if (item.type === "link") {
      toForeground[item.id1] = true;
      toForeground[item.id2] = true;
    } else {
      const neighbours = graph.neighbours(id);
      neighbours.nodes.forEach((neighbourId) => {
        toForeground[neighbourId] = true;
      });
    }

    // Calculate foregrounding based on the nodes - the links will work themselves out.
    chart.foreground((node) => toForeground[node.id]);
  }
}

function getPortGlyph(link, port) {
  const colour = link.d.alert ? colours.alertRed : colours.links;
  return {
    g: [
      {
        t: port,
        c: colour,
        fc: colours.glyphFontColor,
        b: colour,
      },
    ],
  };
}

function showPorts(linkIds) {
  // no link glyphs when using grid arrangements because nodes are too densely
  // packed for the glyphs to be readable
  const withGlyph = getComboShape() === "circle";

  chart.setProperties(
    linkIds.map((id) => {
      const link = chart.getItem(id);
      visiblePorts.push(link);

      return {
        id,
        c: link.d.alert ? colours.alertRed : colours.links,
        t1: withGlyph ? getPortGlyph(link, link.d.port1) : null,
        t2: withGlyph ? getPortGlyph(link, link.d.port2) : null,
        flow: link.d.flow,
      };
    }),
  );

  const reveal = alwaysRevealedLinks.concat(linkIds);
  chart.combo().reveal(reveal);
}

function hidePorts() {
  chart.setProperties(
    visiblePorts.map((link) => ({
      id: link.id,
      c: link.d.alert ? colours.alertRed : colours.links,
      t1: null,
      t2: null,
      flow: false,
    })),
  );

  visiblePorts = [];
  chart.combo().reveal(alwaysRevealedLinks);
}

function select(itemId) {
  hidePorts();

  const item = chart.getItem(itemId);
  if (item) {
    updateUI(item);

    if (chart.combo().isCombo(itemId)) {
      chart.foreground(() => true);
    } else {
      if (item.type === "node") {
        showPorts(graph.neighbours(itemId, { all: true }).links);
      } else {
        showPorts([itemId]);
      }
      updateForeground(itemId);
    }
  }
}

async function setComboShape() {
  const shape = getComboShape();
  chart.options({ combos: { shape } });
  chart.setProperties({ id: ".", sh: shape === "circle" ? null : "box" }, true);

  const selection = chart.selection();
  if (selection) {
    select(selection.pop());
  }

  // Arrange all combos from innermost to outmost to size the new shape appropriately and
  // update the arrangement to take account of the new size of any nested combos
  // get list of all combos
  const comboIds = getComboList();
  await chart.combo().close(comboIds);
  await chart.combo().arrange(comboIds, { name: getComboArrangement() });
  chart.layout("organic", { tightness: 6, mode: "adaptive" });
}

async function setSelectedLabels() {
  const selections = chart.selection();
  const props = [];

  const colourLabel = (item, colour) => {
    let labels = item.t;
    if (Array.isArray(labels)) {
      labels = labels.map((label) => ({ ...label, fbc: colour }));
    } else if (typeof labels !== "string") {
      labels = { ...labels, fbc: colour };
    }

    if (item.oc) {
      item.oc = colourLabel(item.oc);
    }

    return { ...item, t: labels };
  };

  chart.each({ type: "node", items: "all" }, (e) => {
    const { id, t, oc } = e;
    const openComboProp = oc ? { oc } : {};
    const colour = selections.includes(id) ? colours.selection : "transparent";

    props.push({ id, ...colourLabel({ t, ...openComboProp }, colour) });
  });

  await chart.setProperties(props);
}

function initialiseInteractions() {
  document
    .getElementById("comboShape")
    .addEventListener("change", setComboShape);
  // Override default combo open behaviour to
  //  a) Track which combos are currently open
  //  b) Use concentric arrangement

  chart.on("double-click", async ({ id: itemId, preventDefault }) => {
    const item = chart.getItem(itemId);
    if (item && item.type === "node" && chart.combo().isCombo(itemId)) {
      if (chart.combo().isOpen(itemId)) {
        delete openCombos[itemId];
        await chart.combo().close(itemId);

        // only run layout if top-level combo is closed
        if (!item.parentId) {
          await chart.layout("organic", {
            mode: "adaptive",
            tightness: 6,
            consistent: true,
          });
        }
      } else {
        openCombos[itemId] = true;
        chart.combo().open(itemId);
      }
    }
    // disable default doubleclick behaviour
    preventDefault();
  });

  // reveal, foreground and show details for the selected item
  chart.on("selection-change", () => {
    const selections = chart.selection();
    setSelectedLabels();

    if (selections.length) {
      select(selections.pop());
    } else {
      hidePorts();
      chart.foreground(() => true);
      updateUI(null);
    }
  });

  // disable marquee dragging
  chart.on("drag-start", ({ type, preventDefault }) => {
    if (type === "marquee") {
      preventDefault();
    }
  });
}

function pingItem(ids) {
  return chart.ping(ids, { c: colours.alertRed, time: 400 });
}

// returns an array of all the combos that contain a node, starting from the innermost
function ancestors(node) {
  const result = [];
  let nodeId = node.id;
  while (nodeId) {
    const parent = chart.combo().find(nodeId, { parent: "first" });
    if (parent) {
      result.push(parent);
      nodeId = parent;
    } else {
      nodeId = null;
    }
  }
  return result;
}

function zoom(ids) {
  function firstZoomableParent(nodeId) {
    // Get an array of all the node's ancestor combos, starting with the outermost.
    const inwardsCombos = ancestors(chart.getItem(nodeId)).reverse();
    // Find the first that is closed.
    const outermostClosedCombo = inwardsCombos.find(
      (ancestor) => !chart.combo().isOpen(ancestor),
    );
    // If our node has no closed ancestor combos, we can zoom to it directly.
    return outermostClosedCombo || nodeId;
  }
  // We can't zoom to a node if it is within a closed combo.
  // So instead we'll zoom to the outermost of its containing closed parent combos.
  const zoomTargets = ids.map(firstZoomableParent);
  return chart.zoom("fit", { animate: true, time: 800, ids: zoomTargets });
}

// Check whether one or more items is visible within the viewport.
function isVisible(itemIds) {
  if (Array.isArray(itemIds)) {
    return itemIds.every(isVisible);
  }

  const item = chart.getItem(itemIds);
  const view = chart.viewOptions();

  // get the view's bounds in world coordinates (minus an offset)
  const topLeft = chart.worldCoordinates(20, 20);
  const bottomRight = chart.worldCoordinates(view.width - 20, view.height - 20);

  // Check whether the item is within that area
  return (
    item.x >= topLeft.x &&
    item.x <= bottomRight.x &&
    item.y >= topLeft.y &&
    item.y <= bottomRight.y
  );
}

// Reveal, zoom and highlight a particular item
// This 'unwraps' combos layer by layer to show the items inside
async function showAlert(itemId) {
  const layers = []; // unwrap combos one layer at a time
  const toOpen = {};

  // Add any parenting combos for an item to the list of combos to unwrap
  function addLayers(node) {
    ancestors(node).forEach((ancestor, idx) => {
      if (!layers[idx]) {
        layers[idx] = [];
      }
      layers[idx].push(ancestor);
      openCombos[ancestor] = true;
      toOpen[ancestor] = true;
    });
  }

  function arrange(comboids) {
    // Open combos have already been arranged (prior to opening).
    const closedComboIds = comboids.filter(
      (comboId) => !chart.combo().isOpen(comboId),
    );
    return Promise.all(
      closedComboIds.map((comboid) =>
        chart
          .combo()
          .arrange(comboid, { animate: false, name: getComboArrangement() }),
      ),
    );
  }

  // the item ids that we will zoom to fit in the viewport
  let zoomTo = [];
  const item = chart.getItem(itemId);

  async function done() {
    await chart.layout("organic", {
      mode: "adaptive",
      tightness: 6,
      consistent: true,
      fit: false,
      time: 400,
    });
    await zoom(zoomTo);

    if (item.type === "link") {
      return pingItem([item.id, item.id1, item.id2]);
    }
    return pingItem([item.id]);
  }

  // Unwrap one layer of combos (i.e. combos at the same depths)
  async function unwrapLayer() {
    updateForeground(itemId);

    // Get the current topmost layer
    const ids = layers.pop();

    await arrange(ids);
    await chart.combo().open(ids, { time: 800 });
    if (layers.length) {
      return unwrapLayer();
    }
    return done();
  }

  // for a link, we will unwrap and zoom to the nodes at either end
  if (item.type === "link") {
    addLayers(chart.getItem(item.id1));
    addLayers(chart.getItem(item.id2));
    zoomTo = [item.id1, item.id2];
    // for a node, we just unwrap and zoom to the node itself
  } else {
    addLayers(item);
    zoomTo = [itemId];
  }

  select(itemId);

  // Close any combos which we don't need open and clear selection
  const close = [];
  chart.selection([]);

  Object.keys(openCombos).forEach((id) => {
    if (!toOpen[id]) {
      delete openCombos[id];
      close.push(id);
    }
  });
  await chart.combo().close(close, { time: 800 });
  if (close.length > 0) {
    await chart.layout("organic", {
      tightness: 6,
      mode: "adaptive",
      fit: false,
    });
  }

  // Decide whether to zoom to the item before unwrapping the combos
  if (!isVisible(zoomTo)) {
    await zoom([itemId]);
  }
  return unwrapLayer();
}

// Populate the right-hand panel with alert information
function generateAlert(item) {
  const alertsList = document.querySelector(".alerts");
  const button = document.createElement("button");
  button.innerHTML = item.d.alert;
  button.setAttribute("value", item.d.alert);
  button.addEventListener(
    "click",
    async () => {
      const buttons = Array.from(alertsList.children);
      // disable all the buttons during the animation
      buttons.forEach((btn) => {
        btn.disabled = true;
      });
      // run the animation
      await showAlert(item.id);
      // animation is now finished - enable the buttons again
      buttons.forEach((btn) => {
        btn.disabled = false;
      });
    },
    false,
  );

  const li = document.createElement("li");
  li.appendChild(button);
  alertsList.appendChild(button);
}

function populateAlertList() {
  data.items.forEach((item) => {
    if (item.d && item.d.alert) {
      generateAlert(item);
    }
  });
}

async function startKeyLines() {
  const options = {
    drag: {
      links: false,
    },
    logo: { u: "/public/images/Logo.png" },
    selectionColour: colours.selection,
    selectionFontColour: colours.selectionFontColor,
    iconFontFamily: "Font Awesome 5 Free",
    handMode: true,
    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 },
    },
    defaultStyles: {
      comboGlyph: null,
    },
    gradient: {
      stops: [
        { c: colours.backgroundLight, r: 0 },
        { c: colours.backgroundDark, r: 1.0 },
      ],
    },
  };

  chart = await KeyLines.create({ container: "klchart", options });
  populateAlertList();
  initialiseInteractions();

  // Use a graph engine to track relations in the underlying data
  graph = KeyLines.getGraphEngine();
  graph.load(data);

  chart.load(data);
  await setComboShape();
  // 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);
  chart.layout("organic", { tightness: 6, consistent: true });

  updateStyling();
}

async function updateStyling() {
  const getOuterLabels = (place, ip) => [
    {
      t: place,
      position: "s",
      fbc: colours.labelBackground,
      borderRadius: 20,
      bw: 0,
      padding: "3 15 11 15",
      minWidth: 50,
    },
    {
      t: ip,
      fbc: "rgba(0,0,0,0)",
      fs: 8,
      position: "s",
      margin: { top: 18 },
    },
  ];

  const getInnerLabel = (ip) => ({
    t: ip,
    position: "s",
    fbc: colours.labelBackground,
    borderRadius: 20,
    bw: 0,
    padding: "3 5 1 5",
  });

  const props = [];
  chart.each({ type: "node", items: "all" }, ({ id }) => {
    const isCombo = chart.combo().isCombo(id);
    const isOuter = chart.combo().find(id) === null;
    const item = chart.getItem(id);
    if (!isCombo) {
      item.t[0].borderRadius = 20;
      props.push({ id, t: item.t });
      return;
    }

    if (isOuter) {
      const placeName = item.t[0].t;
      const ipAddr = item.t[1].t;
      props.push({
        id,
        t: getOuterLabels(placeName, ipAddr),
        oc: {
          ...item.oc,
          t: getOuterLabels(placeName, ipAddr),
        },
      });
    } else {
      const ip = item.t[0].t;
      props.push({
        id,
        t: getInnerLabel(ip),
        oc: {
          ...item.oc,
          t: getInnerLabel(ip),
        },
      });
    }
  });

  await chart.setProperties(props);
}

function loadFontsAndStart() {
  document.fonts.load('24px "Font Awesome 5 Free"').then(startKeyLines);
}

window.addEventListener("DOMContentLoaded", loadFontsAndStart);
export const colours = {
  primary: "#6AA1C1",
  secondary: "#D6EBF8",
  links: "#4C85A7",
  labelFontColor: "black",

  comboBackgroundRGB: "173, 219, 247",

  alertRed: "#f15d5b",

  glyphBorder: "#a3292d",
  glyphFontColor: "white",

  labelBackground: "transparent",

  selection: "#79bdfc",
  selectionFontColor: "black",

  backgroundLight: "#fcfcfc",
  backgroundDark: "#e6edfc",
};

export const data = {
  items: [
    {
      type: "node",
      id: "140.211.152.43",
      t: [
        {
          t: "Cambridge",
          position: "s",
        },
        {
          t: "140.211.152.43",
        },
      ],
      fbc: "transparent",
      fc: "black",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "network",
        location: "Cambridge",
      },
      fi: {
        t: "fas fa-sitemap",
        c: "#D6EBF8",
      },
      e: 2,
      g: null,
      oc: {
        c: "rgba(173, 219, 247, 0.2)",
        t: "Cambridge\n140.211.152.43",
        bw: 0,
      },
    },
    {
      type: "node",
      id: "192.168.247.0",
      t: {
        t: "192.168.247.0",
      },
      fc: "black",
      fbc: "transparent",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "router",
        network: "140.211.152.43",
        ipPattern: "192.168.247.",
      },
      parentId: "140.211.152.43",
      e: 1.5,
      fi: {
        t: "fas fa-sitemap",
        c: "#D6EBF8",
      },
      g: null,
      oc: {
        bs: "dashed",
        bw: 0,
        c: "rgba(173, 219, 247, 0.2)",
        t: "192.168.247.0",
      },
    },
    {
      id: "140.211.152.43-192.168.247.0",
      type: "link",
      id1: "140.211.152.43",
      id2: "192.168.247.0",
      w: 4,
      d: {
        port1: 443,
        port2: 80,
        flow: {
          velocity: 5.143400773788114,
        },
      },
      c: "#6AA1C1",
      bw: 0,
      fbc: "transparent",
      fc: "black",
    },
    {
      type: "node",
      id: "192.168.218.0",
      t: {
        t: "192.168.218.0",
      },
      fc: "black",
      fbc: "transparent",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "router",
        network: "140.211.152.43",
        ipPattern: "192.168.218.",
      },
      parentId: "140.211.152.43",
      e: 1.5,
      fi: {
        t: "fas fa-sitemap",
        c: "#D6EBF8",
      },
      g: null,
      oc: {
        bs: "dashed",
        bw: 0,
        c: "rgba(173, 219, 247, 0.2)",
        t: "192.168.218.0",
      },
    },
    {
      id: "140.211.152.43-192.168.218.0",
      type: "link",
      id1: "140.211.152.43",
      id2: "192.168.218.0",
      w: 4,
      d: {
        port1: 443,
        port2: 8080,
        flow: {
          velocity: 1.789141649349486,
        },
      },
      c: "#6AA1C1",
      bw: 0,
      fbc: "transparent",
      fc: "black",
    },
    {
      type: "node",
      id: "192.168.16.0",
      t: {
        t: "192.168.16.0",
      },
      fc: "black",
      fbc: "transparent",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "router",
        network: "140.211.152.43",
        ipPattern: "192.168.16.",
      },
      parentId: "140.211.152.43",
      e: 1.5,
      fi: {
        t: "fas fa-sitemap",
        c: "#D6EBF8",
      },
      g: null,
      oc: {
        bs: "dashed",
        bw: 0,
        c: "rgba(173, 219, 247, 0.2)",
        t: "192.168.16.0",
      },
    },
    {
      id: "140.211.152.43-192.168.16.0",
      type: "link",
      id1: "140.211.152.43",
      id2: "192.168.16.0",
      w: 4,
      d: {
        port1: 20,
        port2: 20,
        flow: {
          velocity: 5.636151126524869,
        },
      },
      c: "#6AA1C1",
      bw: 0,
      fbc: "transparent",
      fc: "black",
    },
    {
      type: "node",
      id: "192.168.143.0",
      t: {
        t: "192.168.143.0",
      },
      fc: "black",
      fbc: "transparent",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "router",
        network: "140.211.152.43",
        ipPattern: "192.168.143.",
      },
      parentId: "140.211.152.43",
      e: 1.5,
      fi: {
        t: "fas fa-sitemap",
        c: "#D6EBF8",
      },
      g: null,
      oc: {
        bs: "dashed",
        bw: 0,
        c: "rgba(173, 219, 247, 0.2)",
        t: "192.168.143.0",
      },
    },
    {
      id: "140.211.152.43-192.168.143.0",
      type: "link",
      id1: "140.211.152.43",
      id2: "192.168.143.0",
      w: 4,
      d: {
        port1: 22,
        port2: 22,
        flow: {
          velocity: 0.6168048299616857,
        },
      },
      c: "#6AA1C1",
      bw: 0,
      fbc: "transparent",
      fc: "black",
    },
    {
      type: "node",
      id: "140.211.152.43::192.168.247.240",
      t: {
        t: "192.168.247.240",
      },
      fbc: "transparent",
      fc: "black",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "win",
        network: "140.211.152.43",
        ip: "192.168.247.0",
      },
      fi: {
        t: "fas fa-laptop",
        c: "#D6EBF8",
      },
      parentId: "192.168.247.0",
      e: 1.3,
    },
    {
      id: "192.168.247.0-140.211.152.43::192.168.247.240",
      type: "link",
      id1: "192.168.247.0",
      id2: "140.211.152.43::192.168.247.240",
      w: 4,
      d: {
        port1: 8080,
        port2: 443,
        flow: {
          velocity: 1.6033477650508043,
        },
      },
      c: "#6AA1C1",
      bw: 0,
      fbc: "transparent",
      fc: "black",
    },
    {
      type: "node",
      id: "140.211.152.43::192.168.247.198",
      t: {
        t: "192.168.247.198",
      },
      fbc: "transparent",
      fc: "black",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "win",
        network: "140.211.152.43",
        ip: "192.168.247.0",
      },
      fi: {
        t: "fas fa-laptop",
        c: "#D6EBF8",
      },
      parentId: "192.168.247.0",
      e: 1.3,
    },
    {
      id: "192.168.247.0-140.211.152.43::192.168.247.198",
      type: "link",
      id1: "192.168.247.0",
      id2: "140.211.152.43::192.168.247.198",
      w: 4,
      d: {
        port1: 20,
        port2: 80,
        flow: {
          velocity: 2.8971808260232246,
        },
      },
      c: "#6AA1C1",
      bw: 0,
      fbc: "transparent",
      fc: "black",
    },
    {
      type: "node",
      id: "140.211.152.43::192.168.143.193",
      t: {
        t: "192.168.143.193",
      },
      fbc: "transparent",
      fc: "black",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "printer",
        network: "140.211.152.43",
        ip: "192.168.143.0",
      },
      fi: {
        t: "fas fa-print",
        c: "#D6EBF8",
      },
      parentId: "192.168.143.0",
      e: 1.3,
    },
    {
      id: "192.168.143.0-140.211.152.43::192.168.143.193",
      type: "link",
      id1: "192.168.143.0",
      id2: "140.211.152.43::192.168.143.193",
      w: 4,
      d: {
        port1: 8080,
        port2: 443,
        flow: {
          velocity: 1.3435664831973027,
        },
      },
      c: "#6AA1C1",
      bw: 0,
      fbc: "transparent",
      fc: "black",
    },
    {
      type: "node",
      id: "140.211.152.43::192.168.143.210",
      t: {
        t: "192.168.143.210",
      },
      fbc: "transparent",
      fc: "black",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "phone",
        network: "140.211.152.43",
        ip: "192.168.143.0",
      },
      fi: {
        t: "fas fa-phone",
        c: "#D6EBF8",
      },
      parentId: "192.168.143.0",
      e: 1.3,
    },
    {
      id: "192.168.143.0-140.211.152.43::192.168.143.210",
      type: "link",
      id1: "192.168.143.0",
      id2: "140.211.152.43::192.168.143.210",
      w: 4,
      d: {
        port1: 443,
        port2: 20,
        flow: {
          velocity: 6.724482497272815,
        },
      },
      c: "#6AA1C1",
      bw: 0,
      fbc: "transparent",
      fc: "black",
    },
    {
      type: "node",
      id: "140.211.152.43::192.168.247.66",
      t: {
        t: "192.168.247.66",
      },
      fbc: "transparent",
      fc: "black",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "printer",
        network: "140.211.152.43",
        ip: "192.168.247.0",
      },
      fi: {
        t: "fas fa-print",
        c: "#D6EBF8",
      },
      parentId: "192.168.247.0",
      e: 1.3,
    },
    {
      id: "192.168.247.0-140.211.152.43::192.168.247.66",
      type: "link",
      id1: "192.168.247.0",
      id2: "140.211.152.43::192.168.247.66",
      w: 4,
      d: {
        port1: 20,
        port2: 443,
        flow: {
          velocity: 2.1377476722606614,
        },
      },
      c: "#6AA1C1",
      bw: 0,
      fbc: "transparent",
      fc: "black",
    },
    {
      type: "node",
      id: "140.211.152.43::192.168.16.232",
      t: {
        t: "192.168.16.232",
      },
      fbc: "transparent",
      fc: "black",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "win",
        network: "140.211.152.43",
        ip: "192.168.16.0",
      },
      fi: {
        t: "fas fa-laptop",
        c: "#D6EBF8",
      },
      parentId: "192.168.16.0",
      e: 1.3,
    },
    {
      id: "192.168.16.0-140.211.152.43::192.168.16.232",
      type: "link",
      id1: "192.168.16.0",
      id2: "140.211.152.43::192.168.16.232",
      w: 4,
      d: {
        port1: 8080,
        port2: 80,
        flow: {
          velocity: 2.6992855943925065,
        },
      },
      c: "#6AA1C1",
      bw: 0,
      fbc: "transparent",
      fc: "black",
    },
    {
      type: "node",
      id: "140.211.152.43::192.168.247.78",
      t: {
        t: "192.168.247.78",
      },
      fbc: "transparent",
      fc: "black",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "printer",
        network: "140.211.152.43",
        ip: "192.168.247.0",
      },
      fi: {
        t: "fas fa-print",
        c: "#D6EBF8",
      },
      parentId: "192.168.247.0",
      e: 1.3,
    },
    {
      id: "192.168.247.0-140.211.152.43::192.168.247.78",
      type: "link",
      id1: "192.168.247.0",
      id2: "140.211.152.43::192.168.247.78",
      w: 4,
      d: {
        port1: 20,
        port2: 20,
        flow: {
          velocity: 1.2036817402204614,
        },
      },
      c: "#6AA1C1",
      bw: 0,
      fbc: "transparent",
      fc: "black",
    },
    {
      type: "node",
      id: "140.211.152.43::192.168.16.58",
      t: {
        t: "192.168.16.58",
      },
      fbc: "transparent",
      fc: "black",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "win",
        network: "140.211.152.43",
        ip: "192.168.16.0",
      },
      fi: {
        t: "fas fa-laptop",
        c: "#D6EBF8",
      },
      parentId: "192.168.16.0",
      e: 1.3,
    },
    {
      id: "192.168.16.0-140.211.152.43::192.168.16.58",
      type: "link",
      id1: "192.168.16.0",
      id2: "140.211.152.43::192.168.16.58",
      w: 4,
      d: {
        port1: 80,
        port2: 22,
        flow: {
          velocity: 2.0225040989493204,
        },
      },
      c: "#6AA1C1",
      bw: 0,
      fbc: "transparent",
      fc: "black",
    },
    {
      type: "node",
      id: "140.211.152.43::192.168.247.225",
      t: {
        t: "192.168.247.225",
      },
      fbc: "transparent",
      fc: "black",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "printer",
        network: "140.211.152.43",
        ip: "192.168.247.0",
      },
      fi: {
        t: "fas fa-print",
        c: "#D6EBF8",
      },
      parentId: "192.168.247.0",
      e: 1.3,
    },
    {
      id: "192.168.247.0-140.211.152.43::192.168.247.225",
      type: "link",
      id1: "192.168.247.0",
      id2: "140.211.152.43::192.168.247.225",
      w: 4,
      d: {
        port1: 443,
        port2: 8080,
        flow: {
          velocity: 9.141306612050144,
        },
      },
      c: "#6AA1C1",
      bw: 0,
      fbc: "transparent",
      fc: "black",
    },
    {
      type: "node",
      id: "140.211.152.43::192.168.218.194",
      t: {
        t: "192.168.218.194",
      },
      fbc: "transparent",
      fc: "black",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "win",
        network: "140.211.152.43",
        ip: "192.168.218.0",
      },
      fi: {
        t: "fas fa-laptop",
        c: "#D6EBF8",
      },
      parentId: "192.168.218.0",
      e: 1.3,
    },
    {
      id: "192.168.218.0-140.211.152.43::192.168.218.194",
      type: "link",
      id1: "192.168.218.0",
      id2: "140.211.152.43::192.168.218.194",
      w: 4,
      d: {
        port1: 8080,
        port2: 20,
        flow: {
          velocity: 4.576510996160639,
        },
      },
      c: "#6AA1C1",
      bw: 0,
      fbc: "transparent",
      fc: "black",
    },
    {
      type: "node",
      id: "140.211.152.43::192.168.247.61",
      t: {
        t: "192.168.247.61",
      },
      fbc: "transparent",
      fc: "black",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "printer",
        network: "140.211.152.43",
        ip: "192.168.247.0",
      },
      fi: {
        t: "fas fa-print",
        c: "#D6EBF8",
      },
      parentId: "192.168.247.0",
      e: 1.3,
    },
    {
      id: "192.168.247.0-140.211.152.43::192.168.247.61",
      type: "link",
      id1: "192.168.247.0",
      id2: "140.211.152.43::192.168.247.61",
      w: 4,
      d: {
        port1: 80,
        port2: 80,
        flow: {
          velocity: 7.126381589402159,
        },
      },
      c: "#6AA1C1",
      bw: 0,
      fbc: "transparent",
      fc: "black",
    },
    {
      type: "node",
      id: "140.211.152.43::192.168.218.110",
      t: {
        t: "192.168.218.110",
      },
      fbc: "transparent",
      fc: "black",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "win",
        network: "140.211.152.43",
        ip: "192.168.218.0",
      },
      fi: {
        t: "fas fa-laptop",
        c: "#D6EBF8",
      },
      parentId: "192.168.218.0",
      e: 1.3,
    },
    {
      id: "192.168.218.0-140.211.152.43::192.168.218.110",
      type: "link",
      id1: "192.168.218.0",
      id2: "140.211.152.43::192.168.218.110",
      w: 4,
      d: {
        port1: 20,
        port2: 443,
        flow: {
          velocity: 5.485074888810438,
        },
      },
      c: "#6AA1C1",
      bw: 0,
      fbc: "transparent",
      fc: "black",
    },
    {
      type: "node",
      id: "140.211.152.43::192.168.143.201",
      t: {
        t: "192.168.143.201",
      },
      fbc: "transparent",
      fc: "black",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "win",
        network: "140.211.152.43",
        ip: "192.168.143.0",
      },
      fi: {
        t: "fas fa-laptop",
        c: "#D6EBF8",
      },
      parentId: "192.168.143.0",
      e: 1.3,
    },
    {
      id: "192.168.143.0-140.211.152.43::192.168.143.201",
      type: "link",
      id1: "192.168.143.0",
      id2: "140.211.152.43::192.168.143.201",
      w: 4,
      d: {
        port1: 20,
        port2: 80,
        flow: {
          velocity: 1.8106962198909393,
        },
      },
      c: "#6AA1C1",
      bw: 0,
      fbc: "transparent",
      fc: "black",
    },
    {
      type: "node",
      id: "140.211.152.43::192.168.218.122",
      t: {
        t: "192.168.218.122",
      },
      fbc: "transparent",
      fc: "black",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "win",
        network: "140.211.152.43",
        ip: "192.168.218.0",
      },
      fi: {
        t: "fas fa-laptop",
        c: "#D6EBF8",
      },
      parentId: "192.168.218.0",
      e: 1.3,
    },
    {
      id: "192.168.218.0-140.211.152.43::192.168.218.122",
      type: "link",
      id1: "192.168.218.0",
      id2: "140.211.152.43::192.168.218.122",
      w: 4,
      d: {
        port1: 22,
        port2: 20,
        flow: {
          velocity: 4.401544121170389,
        },
      },
      c: "#6AA1C1",
      bw: 0,
      fbc: "transparent",
      fc: "black",
    },
    {
      type: "node",
      id: "140.211.152.43::192.168.143.47",
      t: {
        t: "192.168.143.47",
      },
      fbc: "transparent",
      fc: "black",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "phone",
        network: "140.211.152.43",
        ip: "192.168.143.0",
      },
      fi: {
        t: "fas fa-phone",
        c: "#D6EBF8",
      },
      parentId: "192.168.143.0",
      e: 1.3,
    },
    {
      id: "192.168.143.0-140.211.152.43::192.168.143.47",
      type: "link",
      id1: "192.168.143.0",
      id2: "140.211.152.43::192.168.143.47",
      w: 4,
      d: {
        port1: 443,
        port2: 80,
        flow: {
          velocity: 8.217968403296961,
        },
      },
      c: "#6AA1C1",
      bw: 0,
      fbc: "transparent",
      fc: "black",
    },
    {
      type: "node",
      id: "140.211.152.43::192.168.247.83",
      t: {
        t: "192.168.247.83",
      },
      fbc: "transparent",
      fc: "black",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "printer",
        network: "140.211.152.43",
        ip: "192.168.247.0",
      },
      fi: {
        t: "fas fa-print",
        c: "#D6EBF8",
      },
      parentId: "192.168.247.0",
      e: 1.3,
    },
    {
      id: "192.168.247.0-140.211.152.43::192.168.247.83",
      type: "link",
      id1: "192.168.247.0",
      id2: "140.211.152.43::192.168.247.83",
      w: 4,
      d: {
        port1: 80,
        port2: 8080,
        flow: {
          velocity: 0.09230926894976621,
        },
      },
      c: "#6AA1C1",
      bw: 0,
      fbc: "transparent",
      fc: "black",
    },
    {
      type: "node",
      id: "140.211.152.43::192.168.218.27",
      t: {
        t: "192.168.218.27",
      },
      fbc: "transparent",
      fc: "black",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "printer",
        network: "140.211.152.43",
        ip: "192.168.218.0",
      },
      fi: {
        t: "fas fa-print",
        c: "#D6EBF8",
      },
      parentId: "192.168.218.0",
      e: 1.3,
    },
    {
      id: "192.168.218.0-140.211.152.43::192.168.218.27",
      type: "link",
      id1: "192.168.218.0",
      id2: "140.211.152.43::192.168.218.27",
      w: 4,
      d: {
        port1: 8080,
        port2: 20,
        flow: {
          velocity: 7.799841275278851,
        },
      },
      c: "#6AA1C1",
      bw: 0,
      fbc: "transparent",
      fc: "black",
    },
    {
      type: "node",
      id: "140.211.152.43::192.168.218.43",
      t: {
        t: "192.168.218.43",
      },
      fbc: "transparent",
      fc: "black",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "win",
        network: "140.211.152.43",
        ip: "192.168.218.0",
      },
      fi: {
        t: "fas fa-laptop",
        c: "#D6EBF8",
      },
      parentId: "192.168.218.0",
      e: 1.3,
    },
    {
      id: "192.168.218.0-140.211.152.43::192.168.218.43",
      type: "link",
      id1: "192.168.218.0",
      id2: "140.211.152.43::192.168.218.43",
      w: 4,
      d: {
        port1: 80,
        port2: 22,
        flow: {
          velocity: 2.0054363769178196,
        },
      },
      c: "#6AA1C1",
      bw: 0,
      fbc: "transparent",
      fc: "black",
    },
    {
      type: "node",
      id: "140.211.152.43::192.168.218.150",
      t: {
        t: "192.168.218.150",
      },
      fbc: "transparent",
      fc: "black",
      c: "#6AA1C1",
      b: null,
      bw: 0,
      d: {
        type: "win",

// ...truncated 33871 lines
<!doctype html>
<html lang="en" style="background-color: #2d383f">
  <head>
    <meta charset="utf-8" />
    <title>Network Alerts</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>
    <script type="module" src="./code.js"></script>
  </body>
</html>
.details label {
  font-weight: bold;
  min-width: 70px;
  display: inline-block;
}

form label:first-child {
  margin-top: 0px;
}

.details ul.value {
  margin-left: 20px;
  margin-top: 5px;
  margin-bottom: 5px;
}

.alerts {
  margin-left: 0;
  padding: 0px;
}

.alerts button {
  margin: 6px;
  border: 1px solid transparent;
}

.field.combo-type label {
  text-transform: capitalize;
}

Terms of use

These terms do not alter or supersede any existing agreements between you (or your employer) and us.

By accessing or using any Content you agree to be bound by these Terms of Use. Please review these terms carefully before using the website.

The contents of this website, including but not limited to any text, code samples, API references, schemas, interactive tools, and other materials (collectively, the 'Content'), are made available for informational and internal evaluation purposes only. All intellectual property rights in the Content are reserved. No licence is granted to use the Content for any commercial purpose, or to copy, distribute, modify, reverse-engineer, or incorporate any part of the Content into any product or service, without our prior written consent.

This Content is provided “as is” and “as available,” without any representations, warranties, or guarantees of any kind, whether express or implied, including but not limited to implied warranties of merchantability, fitness for a particular purpose, non-infringement, or accuracy. To the fullest extent permitted by applicable law, we expressly exclude and disclaim all implied warranties, conditions, and other terms that might otherwise be implied.

We disclaim all liability for any loss or damage, whether direct, indirect, incidental, consequential, or otherwise, arising from any reliance placed on the Content or from your use of it, to the fullest extent permitted by applicable law. By continuing to access or use the Content, you acknowledge and agree to these terms.