Skip to content
  1. Examples

Force layout

This example shows how to use the force-directed layout which is the most common layout used to position nodes in a graph. charge defines the strength with which the nodes repel each other. gravity stands for the global force, pulling the graph to its mass center. elasticity defines how the nodes collide with each other.

ts
import Ogma, { LocateOptions, RawGraph } from '@linkurious/ogma';

const ogma = new Ogma({
  container: 'graph-container',
  options: { interactions: { drag: { enabled: false } } }
});
const padding = 100;
const animationDuration = 300;

// disable node dragging
//ogma.setOptions({ interactions: { drag: { enabled: false }}});

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

// generate random graph
ogma.generate
  .barabasiAlbert({
    nodes: 500,
    m0: 10,
    m: 1
  })
  .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));
    ogma.setGraph(graph);
    return ogma.view.locateGraph({ padding: 100 });
  })
  .then(() => {
    return run({ padding });
  });

const form = document.querySelector<HTMLFormElement>('#params')!;
let callTimer = 0;

let charge: number, gravity: number, elasticity: number, edgeStrength: number;
const update = () => {
  charge = parseFloat(form['charge'].value);
  gravity = parseFloat(form['gravity']!.value);
  elasticity = parseFloat(form['elasticity']!.value);
  edgeStrength = parseFloat(form['edgeStrength']!.value);

  document.getElementById('charge-value')!.innerHTML = charge.toString();
  document.getElementById('gravity-value')!.innerHTML = gravity.toString();
  document.getElementById('elasticity-value')!.innerHTML =
    elasticity.toString();
  document.getElementById('edgeStrength-value')!.innerHTML =
    edgeStrength.toString();
};

form.addEventListener(
  'input',
  () => {
    clearTimeout(callTimer);
    update();
    callTimer = setTimeout(run, 100) as unknown as number;
  },
  true
);

document.querySelector('#randomize')!.addEventListener('click', randomize);
document.querySelector('#run')!.addEventListener('click', () => {
  run();
});

const run = (locate?: LocateOptions) => {
  const start = Date.now();
  return ogma.layouts
    .force({
      //useWebWorker: false,
      charge: charge,
      gravity: gravity,
      elasticity: elasticity,
      locate: locate,
      duration: animationDuration,
      edgeStrength: edgeStrength
    })
    .then(() => {
      const duration = Date.now() - start;
      document.getElementById('time')!.innerHTML =
        'done in ' + ((duration - animationDuration) / 1000).toFixed(2) + 's';
    });
};

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

function 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 });
    }
  }
}

update();
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="charge-value" class="value">5</span>
        <input
          type="range"
          min="0.5"
          max="50"
          value="5"
          name="charge"
          step="0.5"
        />
        Charge
      </label>
      <label>
        <span id="elasticity-value" class="value">0.9</span>
        <input
          type="range"
          min="0"
          max="1.0"
          value="0.9"
          name="elasticity"
          step="0.01"
        />
        Elasticity
      </label>
      <label>
        <span id="gravity-value" class="value">0.01</span>
        <input
          type="range"
          min="0.005"
          max="0.15"
          value="0.01"
          name="gravity"
          step="0.005"
        />
        Gravity
      </label>
      <label>
        <span id="edgeStrength-value" class="value">0.75</span>
        <input
          type="range"
          min="0.0"
          max="30"
          value="0.75"
          name="edgeStrength"
          step="0.25"
        />
        Edge strength
      </label>
      <p>
        <button type="button" id="randomize">Randomize</button
        ><button type="button" id="run">Run</button>
      </p>
      <div id="time"></div>
    </form>
    <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;
}