Skip to content
  1. Examples

Force layout automatic settings new

This example shows how to make force layout decide which settings are the best depending on the graph's size. It uses the autoStop option.

ts
import Ogma, { RawGraph } from '@linkurious/ogma';
import { getBestLayoutOptions } from './force-auto';
const ogma = new Ogma({
  container: 'graph-container',
  options: { interactions: { drag: { enabled: false } } }
});
const animationDuration = 300;
let graphSize = 50;

const run = () => {
  const start = Date.now();
  setLoading('Running layout...');
  const options = getBestLayoutOptions(ogma, {
    locate: true,
    duration: animationDuration
  });
  return ogma.layouts.force(options).then(() => {
    setLoading(false);
    const duration = Date.now() - start;
    document.getElementById('time')!.innerHTML =
      'done in ' + ((duration - animationDuration) / 1000).toFixed(2) + 's';
  });
};

const addLooseNodes = (graph: RawGraph, n: number) => {
  const baseId = graph.nodes.length;
  for (let i = 0; i < n; i++) {
    graph.nodes.push({ id: baseId + i });
  }
};

const addSmallComponents = (graph: RawGraph, n: number, m: number) => {
  for (let i = 0; i < n; i++) {
    const baseId = graph.nodes.length;
    for (let j = 0; j < m + 1; j++) {
      graph.nodes.push({ id: baseId + j });
    }
    for (let k = 1; k < m + 1; k++) {
      graph.edges.push({ source: baseId, target: baseId + k });
    }
  }
};

const setLoading = (message: string | false) => {
  const loading = document.querySelector<HTMLDivElement>('.loading')!;
  if (message) {
    loading.querySelector('.message')!.innerHTML = message;
    loading.style.display = 'block';
  } else {
    loading.style.display = 'none';
  }
};

const randomize = () => {
  const nodes = ogma.getNodes();
  return nodes.setAttributes(
    nodes.map(node => ({
      x: Math.random() * 150,
      y: Math.random() * 150
    }))
  );
};

function setGraph() {
  // generate random graph
  setLoading('Generating graph...');
  // this probabilistic graph generator is quite slow
  // it's important to separate the generating and layout times
  return ogma.generate
    .barabasiAlbert({
      nodes: graphSize
    })
    .then(graph => {
      // add some disconnected components and 'orphan' nodes
      graph.nodes.forEach(node => {
        delete node.attributes;
      });
      addSmallComponents(graph, Math.floor(Math.log(graph.nodes.length)), 5);
      addLooseNodes(graph, Math.floor(graph.nodes.length / 3));
      return ogma.setGraph(graph, { ignoreInvalid: true });
    })
    .then(() => {
      document.getElementById('options')!.innerHTML = Object.entries(
        getBestLayoutOptions(ogma)
      ).reduce((acc, [key, value]) => {
        const isNumber = typeof value === 'number';
        acc += `<li><b>${key}</b>: ${
          isNumber && key !== 'steps' ? Number(value).toFixed(3) : value
        }</li>`;
        return acc;
      }, '');
      return ogma.view.locateGraph({ padding: 100 });
    })
    .then(() => setLoading(false));
}

const update = () => {
  graphSize =
    2 **
    parseFloat(document.querySelector<HTMLInputElement>('input#size')!.value);
  document.getElementById('size-value')!.innerHTML = graphSize.toString();
};

document.querySelector('#size')!.addEventListener('input', update);
document
  .querySelector('#size')!
  .addEventListener('change', () => setGraph().then(run));
document.querySelector('#randomize')!.addEventListener('click', randomize);
document.querySelector('#run')!.addEventListener('click', run);

update();
setGraph().then(() => run());
html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />

    <link type="text/css" rel="stylesheet" href="styles.css" />
  </head>

  <body>
    <div id="graph-container"></div>
    <form class="control" id="params">
      <label>
        <span id="size-value" class="value">50</span>
        <input
          type="range"
          id="size"
          min="4"
          max="12"
          value="1"
          name="charge"
          step="1"
        />
        Graph size
      </label>
      <ul id="options"></ul>
      <p>
        <button type="button" id="randomize">Randomize</button>
        <button type="button" id="run">Run</button>
      </p>
      <div id="time"></div>
    </form>
    <div class="loading">
      <div class="spinner">
        <div></div>
        <div></div>
        <div></div>
        <div></div>
      </div>
      <div class="message">Loading data…</div>
    </div>
    <script type="module" src="index.ts"></script>
  </body>
</html>
css
html,
body {
  font-family: Helvetica, Arial, Helvetica, sans-serif;
  padding: 0;
  margin: 0;
}

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

.control {
  position: absolute;
  top: 20px;
  right: 20px;
  padding: 10px;
  border-radius: 5px;
  box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
  background: #fff;
}

.control label {
  display: block;
  padding: 5px 0;
}

.control p {
  text-align: center;
}

.control .value {
  width: 50px;
  display: inline-block;
  font-family: Georgia, 'Times New Roman', Times, serif;
  font-weight: bold;
}

#time {
  text-align: center;
  font-family: Georgia, 'Times New Roman', Times, serif;
}

li {
  list-style: none;
}

.spinner {
  display: inline-block;
  position: relative;
  width: 80px;
  height: 80px;
}
.spinner div {
  box-sizing: border-box;
  display: block;
  position: absolute;
  width: 64px;
  height: 64px;
  margin: 8px;
  border: 8px solid #3492ff;
  border-radius: 50%;
  animation: spinner 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
  border-color: #3492ff transparent transparent transparent;
}
.spinner div:nth-child(1) {
  animation-delay: -0.45s;
}
.spinner div:nth-child(2) {
  animation-delay: -0.3s;
}
.spinner div:nth-child(3) {
  animation-delay: -0.15s;
}
@keyframes spinner {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

.loading {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 1000;
  text-align: center;
}
ts
import Ogma, { NodeList, ForceLayoutOptions, NodeId } from '@linkurious/ogma';

const buckets = [
  {
    nMin: 0,
    theta: 0.2,
    autoStop: false,
    elasticity: 0.9,
    gpu: true,
    steps: 300
  },
  {
    nMin: 500,
    theta: 0.6,
    gpu: true,
    autoStop: true,
    elasticity: 0.5,
    steps: 300
  },
  {
    nMin: 3000,
    gpu: true,
    theta: 0.85,
    autoStop: true,
    elasticity: 0,
    steps: 250
  },
  {
    nMin: 7000,
    gpu: false,
    theta: 0.88,
    autoStop: true,
    elasticity: 0,
    steps: 225
  },
  {
    nMin: Infinity,
    theta: 1,
    gpu: false,
    autoStop: true,
    elasticity: 0,
    steps: 200
  }
];

function lerp(min: number, max: number, t: number) {
  return (max - min) * t + min;
}

export function getBestLayoutOptions(
  ogma: Ogma,
  options: ForceLayoutOptions = {}
): ForceLayoutOptions {
  const nodes = options.nodes || ogma.getNodes();
  const N = (nodes as NodeList).size || (nodes as NodeId[]).length;
  let i = 0;
  for (i = 0; i < buckets.length; i++) {
    if (buckets[i].nMin > N) break;
  }
  const bMin = buckets[Math.max(i - 1, 0)];
  const bMax = buckets[i];
  const t =
    bMin.nMin + bMax.nMin === Infinity
      ? 1
      : (N - bMin.nMin) / (bMax.nMin - bMin.nMin);
  const steps = Math.round(lerp(bMin.steps, bMax.steps, t));
  const theta = lerp(bMin.theta, bMax.theta, t);
  const gpu = bMin.gpu || false;
  const elasticity = lerp(bMin.elasticity, bMax.elasticity, t);
  const autoStop = bMin.autoStop;
  return {
    steps,
    theta,
    gpu,
    elasticity,
    autoStop,
    ...options,
  };
}