Skip to content
  1. Examples

Force layout with custom node masses and edge weights new

This example shows how you can control individual node masses and edge weights in a force-directed layout in order to express different properties of your dataset. It allows you to control the strength of the forces that act on the nodes and edges in the layout, which can be useful for emphasizing certain aspects of the data.

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

interface NodeData {
  importance: number;
}

interface EdgeData {
  weight: number;
}

const ogma = new Ogma<NodeData, EdgeData>({
  container: 'graph-container',
  options: {
    backgroundColor: '#505050'
  }
});

(async () => {
  let gravity: number | undefined = 0;
  let theta: number | undefined = 0.64;
  let steps: number | undefined = 300;
  async function runLayout() {
    await ogma.layouts.force({
      steps,
      locate: true,
      gravity,
      gpu: true,
      nodeMass: n => n.getData('importance'),
      edgeWeight: e => e.getData('weight')
    });
  }

  //await runLayout();

  const examples = {
    async arch() {
      gravity = 0;
      theta = undefined;
      steps = 600;
      ogma.styles.addRule({
        nodeAttributes: {
          radius: n => n.getData('importance') * 5,
          text: {
            content: n => Math.round(n.getData('importance')),
            color: '#bbb',
            backgroundColor: '#333',
            minVisibleSize: 5,
            tip: false
          }
        },
        edgeAttributes: {
          width: e => e.getData('weight') * 5,
          color: 'white',
          text: {
            color: 'white',
            content: edge => {
              const weight = edge.getData('weight');
              return weight === 1 ? '1' : weight.toFixed(2);
            },
            minVisibleSize: 0
          }
        }
      });
      await ogma.setGraph({
        nodes: Array.from({ length: 10 }, (_, i) => ({
          id: i,
          data: { importance: i < 5 ? i + 1 : 10 - i }
        })),
        edges: [
          ...Array.from({ length: 9 }, (_, i) => ({
            source: i,
            target: i + 1,
            data: { weight: 1 }
          })),
          { source: 9, target: 0, data: { weight: 0.05 } }
        ]
      });
      await runLayout();
    },

    async grid() {
      gravity = 0;
      theta = undefined;
      const graph = await ogma.generate.grid({ rows: 5, columns: 5 });
      await ogma.setGraph(graph);
      await ogma.view.locateGraph({ padding: 150 });

      ogma.getNodes().forEach((node, i) => {
        const id = node.getId();
        // cornder nodes are the most important
        const importance =
          id === 0 || id === 4 || id === 20 || id === 24 ? 10 : 0.5;
        node.setData('importance', importance);
      });
      ogma.getEdges().forEach(edge => {
        edge.setData(
          'weight',
          10 /
            Math.max(
              edge.getSource().getData('importance') +
                edge.getTarget().getData('importance')
            )
        );
      });
      ogma.styles.addRule({
        nodeAttributes: {
          radius: n => 5 + n.getData('importance'),
          text: {
            content: n => n.getData('importance').toFixed(2),
            minVisibleSize: 50,
            font: 'Georgia',
            color: 'white'
          }
        },
        edgeAttributes: {
          width: e => e.getData('weight') / 5,
          text: {
            color: 'white',
            minVisibleSize: 0,
            font: 'Georgia',
            content: edge => {
              const weight = edge.getData('weight');
              if (weight < 1) return weight.toFixed(2);
              return undefined;
            }
          }
        }
      });
    },

    async groups() {
      gravity = 0.1;
      theta = undefined;

      const input = await Ogma.parse.gexfFromUrl('files/arctic.gexf');
      const nodesByColor = new Map<NodeId, string>();
      input.nodes.forEach(node => {
        nodesByColor.set(node.id!, node.attributes!.color as string);
        (node.data as NodeData).importance = 1;
      });

      input.edges.forEach(edge => {
        // if edges belong to the same group, increase weight
        const sameGroup =
          nodesByColor.get(edge.source) === nodesByColor.get(edge.target);
        (edge.data as EdgeData) = { weight: sameGroup ? 20 : 0.1 };
      });

      ogma.styles.addRule({
        nodeAttributes: {},
        edgeAttributes: {
          width: 1,
          color: e => {
            const source = e.getSource().getId();
            const target = e.getTarget().getId();
            if (nodesByColor.get(source) === nodesByColor.get(target))
              return nodesByColor.get(source);
            return 'rgba(0, 0, 0, 0.05)';
          }
        }
      });

      await ogma.setGraph(input as RawGraph<NodeData, EdgeData>);
      await ogma.view.locateGraph({ padding: 150 });
      await ogma.removeNodes(ogma.getNodes().filter(n => n.getDegree() === 0));
    },

    async layers() {
      gravity = 0;
      theta = 0.34;
      steps = 600;
      const graph = await ogma.generate.random({ nodes: 500, edges: 0 });
      await ogma.setGraph(graph);
      await ogma.view.locateGraph();
      // 5 contrast pastel colors
      const colors = ['#FFC0CB', '#FFD700', '#98FB98', '#87CEEB', '#FFA07A'];
      ogma.styles.addRule({
        nodeAttributes: {
          radius: n => n.getData('importance') * 5,
          color: n => colors[n.getData('importance') - 1]
        }
      });
      await ogma.addEdges(
        ogma.getNodes().map((n, i) => ({ source: i, target: 0 }))
      );
      await ogma.getNodes().forEach((node, i) => {
        node.setData('importance', Math.floor(i / 100) + 1);
      });
      await ogma.view.locateGraph({ padding: 150 });
      await runLayout();
    },

    async centrality() {
      gravity = undefined;
      theta = 0.64;
      const graph = await ogma.generate.barabasiAlbert({
        nodes: 256
      });
      await ogma.setGraph(graph);
      ogma.getNodes().setData(
        'importance',
        ogma.algorithms
          .betweenness({
            directed: false,
            normalized: true
          })
          .map(v => 1 + 20 * v)
      );
      ogma.getEdges().setData('weight', edge => {
        const source = edge.getSource();
        const target = edge.getTarget();
        if (
          source.getData('importance') > 1 &&
          target.getData('importance') > 1
        )
          return 10;
        return 1;
      });

      await ogma.styles
        .addRule({
          nodeAttributes: {
            radius: n => n.getData('importance') * 10
          },
          edgeAttributes: {
            width: e => (e.getData('weight') <= 1 ? 0.1 : 10)
          }
        })
        .whenApplied();
      await ogma.view.locateGraph({ padding: 150 });
      await runLayout();
    }
  };

  const state = {
    runLayout,
    example: 'layers' as keyof typeof examples,
    examples
  };

  // @ts-expect-error dat.GUI
  const gui = new dat.GUI();

  gui
    .add(state, 'example', ['layers', 'grid', 'groups', 'arch', 'centrality'])
    .name('Example')
    .onChange(async () => {
      ogma.styles.getRuleList().forEach(rule => rule.destroy());
      steps = 300;
      await ogma.clearGraph();
      await state.examples[state.example]();
      //await runLayout();
    });
  gui.add(state, 'runLayout').name('Run layout');

  await examples.layers();
})();
html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <script src="https://cdn.jsdelivr.net/npm/dat.gui@0.7.9/build/dat.gui.min.js"></script>
    <link type="text/css" rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <div id="graph-container"></div>
    <script src="index.ts"></script>
  </body>
</html>
css
#graph-container {
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  position: absolute;
  margin: 0;
  overflow: hidden;
}