Skip to content
  1. Examples

Performance Workbench

In this demo it is possible to stress test Ogma on the performance side.
Load from tens of nodes up to thousands and check layout or animation effect on performances.

FPS is the measure used to measure performance here:

  • 60 FPS is the best value,
  • any value between 60 and 30 is considered quite good,
  • values below 30 FPS are a bad indicator.
js
import Ogma from '@linkurious/ogma';
const addStatsPanel = () => {
  const stats = new Stats();
  document.querySelector('#stats').appendChild(stats.domElement);
  const loop = () => {
    stats.update();
    requestAnimationFrame(loop);
  };
  requestAnimationFrame(loop);
};

const ogma = new Ogma({
  container: 'graph-container'
});

// add the stats utility
addStatsPanel();

const SIZES = {
  small: 50,
  medium: 500,
  big: 3000
};
const COLOR_STEPS = ['#c7e9c0', '#74c476', '#238b45', '#00441b'];
const minVisibleSize = 0;
const w = document.documentElement.clientWidth;
const h = document.documentElement.clientHeight;
let isAnimated = false;

const loadGraph = size =>
  ogma.generate
    .randomTree({ nodes: size })
    .then(graph => {
      const nodes = graph.nodes;
      const edges = graph.edges;
      nodes.forEach((n, i) => {
        n.attributes = {
          text: { content: 'Node text ' + i, minVisibleSize },
          radius: 5,
          x: Math.random() * w,
          y: Math.random() * h,
          color: COLOR_STEPS[0],
          outerStroke: {
            scalingMethod: 'scaled',
            width: 1,
            color: COLOR_STEPS[3]
          },
          innerStroke: {
            width: 1,
            scalingMethod: 'scaled'
          },
          outline: false,
          icon: {
            font: 'Font Awesome 5 Free',
            content: '\uf007',
            style: 'bold',
            color: '#0f2560'
          }
        };
      });
      edges.forEach((e, i) => {
        e.attributes = {
          text: 'Edge text ' + i,
          color: COLOR_STEPS[4]
        };
      });
      return { nodes: nodes, edges: edges };
    })
    .then(graph => {
      return ogma.setGraph(graph);
    })
    .then(() => {
      return ogma.view.locateGraph();
    });

const runLayout = () => {
  const start = Date.now();
  toggleLoader(true);
  return ogma.layouts
    .force({
      locate: { padding: 50 }
    })
    .then(() => {
      toggleLoader(false);
      updateLayoutTiming(Date.now() - start);
    });
};

// Animate nodes and edges by position and colors
const animate = i => {
  const newColor = COLOR_STEPS[i] || COLOR_STEPS[0];
  const nodes = ogma.getNodes();
  const edges = ogma.getEdges();
  const duration = 3000;

  if (isAnimated) {
    ogma.styles.setNodeTextsVisibility(false);
    return Promise.all([
      edges.setAttribute('color', newColor, duration),
      nodes.setAttributes(
        {
          x: () => Math.random() * w,
          y: () => Math.random() * h,
          color: newColor
        },
        duration
      )
    ]).then(() => {
      return animate((i + 1) % COLOR_STEPS.length);
    });
  } else {
    ogma.styles.setNodeTextsVisibility(true);
  }
};

const toggleLoader = show => {
  document.querySelector('.loader').style.display = show ? 'block' : 'none';
};

const updateLayoutTiming = duration => {
  const formattedTime = (duration / 1000).toFixed(2);
  const message =
    'Layout time: ' +
    formattedTime +
    's for ' +
    ogma.getNodes().size +
    ' nodes';
  document.querySelector('#layout-stats').innerText = message;
};

const updateAnimateButton = () => {
  const newLabel = isAnimated ? 'Stop animation' : 'Animate';
  document.querySelector('#animate').innerText = newLabel;
};

document.querySelectorAll('button[name="dataset"]').forEach(button => {
  button.addEventListener(
    'click',
    evt => {
      isAnimated = false;
      updateAnimateButton();

      evt.preventDefault();
      const id = evt.target.id;
      const size = SIZES[id];
      loadGraph(size);
    },
    false
  );
});

document.querySelector('#layout').addEventListener(
  'click',
  () => {
    isAnimated = false;
    updateAnimateButton();
    runLayout();
  },
  false
);

document.querySelector('#animate').addEventListener(
  'click',
  evt => {
    isAnimated = evt.target.innerText === 'Animate';
    animate(0);
    updateAnimateButton();
  },
  false
);

const switches = document.querySelectorAll('#ui [name=renderer-switch]');

switches.forEach(input => {
  input.addEventListener('click', evt => {
    const mode = evt.target.value;
    ogma.setOptions({ renderer: mode });
  });
});

ogma.events.on('rendererStateChange', evt => {
  // Fallback to Canvas if WebGL is not supported
  if (evt.code === 'NO_WEBGL') {
    switches.forEach(input => {
      input.checked = input.value === 'canvas';
      input.disabled = input.value !== 'canvas';
    });
  }
});

loadGraph(SIZES.medium).then(runLayout);
html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/stats.js/7/Stats.min.js"></script>
    <link type="text/css" rel="stylesheet" href="styles.css">
    <link
      rel="stylesheet"
      type="text/css"
      href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.min.css"
    />
  </head>

  <body>
    <div id="graph-container"></div>
    <div class="toolbar" id="ui">
      <div class="switch switch--horizontal">
        <input
          type="radio"
          name="renderer-switch"
          value="webgl"
          checked="checked"
          autocomplete="off"
        />
        <label for="webgl">WebGL</label>
        <input
          type="radio"
          name="renderer-switch"
          value="canvas"
          autocomplete="off"
        />
        <label for="canvas">Canvas</label>
        <span class="toggle-outside">
          <span class="toggle-inside"></span>
        </span>
      </div>
      <div class="controls">
        <button id="small" name="dataset" class="btn menu">50 nodes</button>
        <button id="medium" name="dataset" class="btn menu">500 nodes</button>
        <button id="big" name="dataset" class="btn menu">3000 nodes</button>
      </div>
      <div class="controls">
        <h4>Stress tests</h4>
        <button id="animate" class="btn menu">Animate</button>
        <button id="layout" class="btn menu">Layout</button>
      </div>
      <h4>Metrics</h4>
      <div id="stats"></div>
      <div id="layout-stats">Layout time: ...</div>
    </div>
    <div class="loader">
      <div class="spinner">
        <div class="double-bounce1"></div>
        <div class="double-bounce2"></div>
      </div>
    </div>
    <script src="index.js"></script>
  </body>
</html>
css
body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
}

#graph-container {
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  position: absolute;
  margin: 0;
  overflow: hidden;
}

#ui {
  text-align: center;
}

h4 {
  margin: 15px 5px 5px 5px;
}

.toolbar {
  display: block;
  position: absolute;
  top: 20px;
  right: 20px;
  padding: 10px;
  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65);
  border-radius: 4px;
  background: #ffffff;
  color: #222222;
  font-weight: 300;
}

.controls {
  text-align: center;
  margin-top: 5px;
}

#stats {
  padding: 20px;
}

#stats div {
  margin: 0 auto;
  width: 80px;
}

.btn {
  padding: 6px 8px;
  background-color: white;
  cursor: pointer;
  font-size: 18px;
  border: none;
  border-radius: 5px;
  outline: none;
}

.btn:hover {
  color: #333;
  background-color: #e6e6e6;
}

.menu {
  border: 1px solid #ddd;
  width: 80%;
  font-size: 14px;
  margin-top: 10px;
}

.loader {
  display: block;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(255, 255, 255, 0.5);
}

.spinner {
  width: 80px;
  height: 80px;

  position: absolute;
  top: 50%;
  left: 50%;
  -moz-transform: translateX(-50%) translateY(-50%);
  -webkit-transform: translateX(-50%) translateY(-50%);
  transform: translateX(-50%) translateY(-50%);
}

.double-bounce1,
.double-bounce2 {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background-color: #00441b;
  opacity: 0.6;
  position: absolute;
  top: 0;
  left: 0;

  -webkit-animation: sk-bounce 2s infinite ease-in-out;
  animation: sk-bounce 2s infinite ease-in-out;
}

.double-bounce2 {
  -webkit-animation-delay: -1s;
  animation-delay: -1s;
}

@-webkit-keyframes sk-bounce {
  0%,
  100% {
    -webkit-transform: scale(0);
  }

  50% {
    -webkit-transform: scale(1);
  }
}

@keyframes sk-bounce {
  0%,
  100% {
    transform: scale(0);
    -webkit-transform: scale(0);
  }

  50% {
    transform: scale(1);
    -webkit-transform: scale(1);
  }
}

.switch {
  width: 100%;
  position: relative;
}

.switch input {
  position: absolute;
  top: 0;
  z-index: 2;
  opacity: 0;
  cursor: pointer;
}

.switch input:checked {
  z-index: 1;
}

.switch input:checked + label {
  opacity: 1;
  cursor: default;
}

.switch input:not(:checked) + label:hover {
  opacity: 0.5;
}

.switch label {
  color: #222222;
  opacity: 0.33;
  transition: opacity 0.25s ease;
  cursor: pointer;
}

.switch .toggle-outside {
  height: 1.5rem;
  border-radius: 2rem;
  padding: 0.25rem;
  overflow: hidden;
  transition: 0.25s ease all;
}

.switch .toggle-inside {
  border-radius: 2.5rem;
  background: #4a4a4a;
  position: absolute;
  transition: 0.25s ease all;
}

.switch--horizontal {
  width: 15rem;
  height: 2rem;
  margin: 0 auto;
  font-size: 0;
  margin-bottom: 1rem;
}

.switch--horizontal input {
  height: 2rem;
  width: 5rem;
  left: 5rem;
  margin: 0;
}

.switch--horizontal label {
  font-size: 1rem;
  line-height: 2rem;
  display: inline-block;
  width: 5rem;
  height: 100%;
  margin: 0;
  text-align: center;
}

.switch--horizontal label:last-of-type {
  margin-left: 5rem;
}

.switch--horizontal .toggle-outside {
  background: #dddddd;
  position: absolute;
  width: 4.5rem;
  left: 5rem;
}

.switch--horizontal .toggle-inside {
  height: 1.5rem;
  width: 1.5rem;
}

.switch--horizontal input:checked ~ .toggle-outside .toggle-inside {
  left: 0.25rem;
}

.switch--horizontal input ~ input:checked ~ .toggle-outside .toggle-inside {
  left: 3.25rem;
}

.switch--horizontal input:disabled ~ .toggle-outside .toggle-inside {
  background: #9a9a9a;
}

.switch--horizontal input:disabled ~ label {
  color: #9a9a9a;
}