Search

Test Performance

Large Charts

Measure how well KeyLines performs.

Test Performance
View live example →

You will need to know how KeyLines performs.

This demo is designed for you to test its performance on your target hardware, browser and OS combination.

There are two main ways of measuring performance: frame rate and running layouts.

Frame Rate

The frame rate is the number of frames the browser is able to render per second. It is a critical measure of the responsiveness of an application from a user's perspective.

Browsers typically 'cap' their frame rates at 60 frames a second, so KeyLines can't draw faster than that. At sixty frames a second users cannot perceive individual frames and the experience is smooth. If the frame rate drops below 5 then users will start to complain when they are performing interactive tasks such as dragging nodes or selecting them.

The KeyLines' frame rate is roughly linear with the number of items drawn. I.e., drawing twice the number of items will typically halve the frame rate.

Layouts

KeyLines layouts are very fast, out-performing open source alternatives.

The demo uses our ‘organic’ layout, which has excellent performance on large datasets.

What happens when KeyLines isn't fast enough?

In general your users will struggle to comprehend networks of thousands of nodes and links, so sometimes it is worth thinking about things from their perspective and designing to that.

There are two basic techniques to get around this problem: filtering the network, and aggregating nodes together.

For more details, see Performance.

import KeyLines from "keylines";

function lerp(a, b, t) {
  return (1 - t) * a + t * b;
}

/**
 * Stars an FPS counter recording exponentially weighted mean and
 * standard deviation.
 */
function FpsCounter() {
  let tick = -1;
  let mean = -1;
  let sd = 0;

  (function next() {
    requestAnimationFrame((tock) => {
      if (tick > 0) {
        const durr = tock - tick;

        sd = mean > 0 ? lerp(sd, (mean - durr) ** 2, 0.06) : 0;
        mean = lerp(mean, durr, 0.02);
      }
      tick = tock;
      next();
    });
  })();

  return {
    get: () => {
      const fps = 1000 / mean;
      const fpsStd = (1000 * Math.sqrt(sd)) / (mean * mean);

      return { mean: fps, std: fpsStd };
    },
  };
}

var width = 638;
var height = 470;
var numNodes;
var numLinks;
var spacing;
var zooming = false;
var zoomHow = "out";
var animating = false;
var showLabels = false;
var chart;

function pushData(loadedChart) {
  chart = loadedChart;

  function linkIds() {
    var obj = {
      id1: "id" + Math.floor(Math.random() * numNodes),
      id2: "id" + Math.floor(Math.random() * numNodes),
    };
    if (obj.id1 === obj.id2) {
      return linkIds(numNodes); // try again
    }

    return obj;
  }

  function randomX() {
    return Math.floor(Math.random() * width * spacing);
  }

  function randomY() {
    return Math.floor(Math.random() * height * spacing);
  }

  function random(nNodes, nLinks) {
    cancelAnimations();
    numNodes = nNodes;
    numLinks = nLinks;
    spacing = numNodes < 150 ? 1 : Math.sqrt(numNodes / 150);

    var nodes = (function () {
      var ret = [];
      for (var i = 0; i < numNodes; i++) {
        ret.push({
          id: "id" + i,
          type: "node",
          t: showLabels ? i : null,
          d: { label: i },
          x: randomX(),
          y: randomY(),
          u: "/images/icons/telephone.png",
        });
      }
      return ret;
    })();

    var links = (function () {
      var ret = [];
      for (var i = 0; i < numLinks; i++) {
        var ids = linkIds();
        ret.push({
          id: "linkid" + i,
          type: "link",
          t: showLabels ? i : null,
          d: { label: i },
          id1: ids.id1,
          id2: ids.id2,
          c: "rgb(50, 50, 50)",
          w: 1,
        });
      }
      return ret;
    })();

    var items = nodes.concat(links);

    chart.load({
      type: "LinkChart",
      items: items,
    });
    chart.zoom("fit");
  }

  async function startAnimation() {
    if (animating) {
      var items = [];
      for (var i = 0; i < numNodes; i++) {
        items.push({ id: "id" + i, x: randomX(), y: randomY() });
      }
      await chart.animateProperties(items, { time: 3000 });
      startAnimation();
    }
  }

  function swapButtons(type, isActive) {
    // cast the isActive because undefined is different from false in this case
    $("#" + type + "On").toggleClass("active btn-kl", !!isActive);
    $("#" + type + "Off").toggleClass("active btn-kl", !isActive);
  }

  $("#animationOff").click(function (evt) {
    animating = false;
    swapButtons("animation", false);

    evt.preventDefault();
  });

  $("#animationOn").click(function (evt) {
    if (numNodes > 0 && animating === false) {
      swapButtons("animation", true);
      animating = true;
      startAnimation();
    }

    evt.preventDefault();
  });

  async function startZooming() {
    if (zooming) {
      // Invert the current zoom
      zoomHow = zoomHow === "in" ? "out" : "in";
      // Run the zoom
      await chart.zoom(zoomHow, { animate: true });
      startZooming();
    }
  }

  $("#zoomOff").click(function (evt) {
    zooming = false;
    swapButtons("zoom", false);

    evt.preventDefault();
  });

  $("#zoomOn").click(function (evt) {
    if (numNodes > 0 && zooming === false) {
      zooming = true;
      swapButtons("zoom", true);
      startZooming();
    }

    evt.preventDefault();
  });

  function toggleLabels() {
    var items = [];
    chart.each({ type: "all" }, function (item) {
      items.push({ id: item.id, t: showLabels ? item.d.label : null });
    });
    chart.setProperties(items);
  }

  $("#labelsOff").click(function (evt) {
    showLabels = false;
    swapButtons("labels", false);
    toggleLabels();
    evt.preventDefault();
  });

  $("#labelsOn").click(function (evt) {
    if (numNodes > 0) {
      showLabels = true;
      swapButtons("labels", true);
      toggleLabels();
    }

    evt.preventDefault();
  });

  function cancelAnimations() {
    // Cancel nodes animation
    animating = false;
    swapButtons("animation", false);
    // Cancel zoom
    zooming = false;
    swapButtons("zoom", false);
  }

  $("#generate").click(function (evt) {
    var option = $("#nodelink").val();
    if (option) {
      $("#animationOn").removeClass("disabled");
      $("#zoomOn").removeClass("disabled");
      $("#labelsOn").removeClass("disabled");
      var parts = option.split(",");
      random(Number(parts[0]), Number(parts[1]));
    }

    evt.preventDefault();
  });

  $("#layout").click(async function (evt) {
    cancelAnimations();
    evt.preventDefault();
    var start = new Date().getTime();
    await chart.layout("organic", { animate: false, packing: "circle" });
    var endtime = new Date().getTime();
    var secs = (endtime - start) / 1000;
    $("#layoutStats").text(secs + "s");
  });

  // display frame rate
  setInterval(
    function (fps) {
      let { mean, std } = fps.get();
      // fps +/- half standard deviation
      // n.b. filters extreme standard deviations (e.g. if the frame rate drops to zero
      // for several frames)
      const range = std > mean / 2 ? "-" : Math.round(std);
      $("#fps").text(Math.round(mean) + ` (± ${range}) Frames per Second`);
    },
    1000,
    FpsCounter()
  );
}

$(function () {
  var options = {
    logo: { u: "/images/Logo.png" },
    minZoom: 0.02,
  };
  KeyLines.create({ container: "klchart", options: options }).then(pushData);
});
<!doctype html>
<html lang="en" style="background-color: #2d383f">
  <head>
    <meta charset="utf-8" />
    <title>Test Performance</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" />
    <style>
      h5 {
        margin-bottom: 0;
      }
    </style>
    <script src="jquery" defer type="text/javascript"></script>
  </head>
  <body>
    <div id="klchart" class="klchart"></div>
    <script type="module" src="./code.js"></script>
  </body>
</html>

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.