//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//
//     Sample Code
//
//!    Measure how well KeyLines performs.
import KeyLines from "keylines";
function fpsCounter(blend = 0.33) {
  var tick = Date.now();
  var mean = -1;

  (function next() {
    requestAnimationFrame((tock) => {
      var durr = tock - tick;
      mean = mean >= 0 ? mean * blend + durr * (1 - blend) : durr;
      tick = tock;
      next();
    });
  })();

  return { get: () => Math.round(1000 / mean) };
}

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) {
      $("#fps").text(fps.get() + " Frames per Second");
    },
    400,
    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="/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">
    <style>
      h5 {
        margin-bottom: 0;
      }
      
    </style>
    <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="/performance.js" defer type="text/javascript"></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" id="klchart">
          </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%;">Test Performance</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">
                <h3 id="fps">0 Frames per Second</h3>
                <select class="medium" id="nodelink">
                  <option value="10,10">10 Nodes, 10 Links</option>
                  <option value="50,50">50 Nodes, 50 Links</option>
                  <option value="100,100">100 Nodes, 100 Links</option>
                  <option value="200,200">200 Nodes, 200 Links</option>
                  <option value="500,500" selected="true">500 Nodes, 500 Links</option>
                  <option value="1000,1000">1,000 Nodes, 1,000 Links</option>
                  <option value="2000,2000">2,000 Nodes, 2,000 Links</option>
                  <option value="5000,5000">5,000 Nodes, 5,000 Links</option>
                  <option value="10000,10000">10,000 Nodes, 10,000 Links</option>
                  <option value="20000,20000">20,000 Nodes, 20,000 Links</option>
                </select>
                <p>
                  <input class="btn" type="button" value="Generate" id="generate">
                </p>
                <p>
                  <input class="btn" type="button" value="Layout" id="layout"><span id="layoutStats" style="margin-left:10px;"></span>
                </p>
                <p>
                  <fieldset>
                    <legend>Options</legend>
                    <h5>Animate node positions:</h5>
                    <div class="btn-group">
                      <input class="btn disabled" type="button" value="On" id="animationOn">
                      <input class="btn btn-kl active" type="button" value="Off" id="animationOff">
                    </div>
                    <h5>Zoom the chart in and out:</h5>
                    <div class="btn-group">
                      <input class="btn disabled" type="button" value="On" id="zoomOn">
                      <input class="btn btn-kl active" type="button" value="Off" id="zoomOff">
                    </div>
                    <h5>Show labels:</h5>
                    <div class="btn-group">
                      <input class="btn disabled" type="button" value="On" id="labelsOn">
                      <input class="btn btn-kl active" type="button" value="Off" id="labelsOff">
                    </div>
                  </fieldset>
                </p>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
    <div id="moreParent">
      <div id="moreContainer">
      </div>
    </div>
    <div id="lazyScripts">
    </div>
  </body>
</html>
Loading source