Skip to content
  1. Examples

Visual grouping highlight

Hover a node to see how it's connections are highlighted.

ts
import Ogma, {
  Color as ColorOrNull,
  NodeId,
  Node,
  NodeList,
  EdgeList
} from '@linkurious/ogma';
import chroma from 'chroma-js';

type Color = NonNullable<ColorOrNull>;

const formatRGBA = (color: [number, number, number, number]) =>
  `rgba(${color.join(', ')})`;

const ogma = new Ogma({
  container: 'graph-container',
  options: {
    backgroundColor: '#f5f6f6'
  }
});
const names: Record<Color, string> = {
  'rgba(51, 153, 255, 1)': 'Arctic, communities',
  'rgba(0, 204, 204, 1)': 'Research',
  'rgba(153, 255, 255, 1)': 'Population',
  'rgba(255, 204, 51, 1)': 'Aerospace',
  'rgba(255, 204, 102, 1)': 'Geosciences',
  'rgba(255, 255, 51, 1)': 'Physics',
  'rgba(102, 0, 102, 1)': 'Petrol',
  'rgba(153, 0, 0, 1)': 'Political Studies',
  'rgba(102, 102, 0, 1)': 'Pollution',
  'rgba(0, 153, 0, 1)': 'Energy resources',
  'rgba(153, 255, 0, 1)': 'Biodiversity',
  'rgba(0, 204, 51, 1)': 'Global warming'
};

function applyGrouping() {
  const transformation = ogma.transformations.addNodeGrouping({
    groupIdFunction: node => node.getData('groupId'),
    nodeGenerator: (nodes, groupId) => ({
      id: 'special group ' + groupId,
      data: {
        groupId: groupId
      },
      attributes: {
        text: names[groupId] || 'Other',
        color: formatRGBA(chroma(groupId).brighten(0.7).rgba()),
        opacity: 0.82
      }
    }),
    edgeGenerator: (edges, id) => ({
      id: id,
      attributes: {
        width: 1 + Math.log(edges.size)
      }
    }),
    showContents: metaNode => true,
    onGroupUpdate: (_, subNodes, open) => {
      if (open) {
        return ogma.layouts.force({
          nodes: subNodes,
          duration: 0
        }) as unknown as Promise<void>;
      }
    }
  });
  return transformation.whenApplied().then(() => {
    ogma.layouts.force({
      steps: 150,
      charge: 0.125,
      gravity: 0.01,
      edgeStrength: 1,
      theta: 0.9,
      locate: true
    });
  });
}

const graph = await Ogma.parse.gexfFromUrl('files/arctic.gexf');
await ogma.setGraph(graph);
await ogma.view.locateGraph();
const byColor = ogma
  .getNodes()
  .getAttribute('color')
  .reduce(
    (acc, color) => {
      acc[color as Color] = [];
      return acc;
    },
    {} as Record<Color, NodeId[]>
  );
ogma.getNodes().forEach(n => {
  byColor[n.getAttribute('color') as Color].push(n.getId());
});
Object.keys(byColor).forEach(color => {
  ogma.getNodes(byColor[color]).fillData('groupId', color);
});
await applyGrouping();

ogma.styles.setHoveredNodeAttributes({ outline: false });
ogma.styles.setSelectedNodeAttributes({ outline: false });
ogma.styles.addRule({
  nodeAttributes: {
    innerStroke: { width: 0.1, scalingMethod: 'scaled', color: '#555' }
  },
  edgeAttributes: {
    color: '#555'
  }
});
interface State {
  hoveredNode: null | Node;
  edgesToHighlight: null | EdgeList;
  nodesToHighlight: null | NodeList;
  edgesToDim: null | EdgeList;
  edgesVisibility: boolean[];
  nodesToDim: null | NodeList;
}
const state: State = {
  hoveredNode: null,
  edgesToHighlight: null,
  nodesToHighlight: null,
  edgesToDim: null,
  edgesVisibility: [],
  nodesToDim: null
};

ogma.styles.createClass({
  name: 'dimmed',
  nodeAttributes: { opacity: 0.15 },
  edgeAttributes: { opacity: 0.15 }
});

ogma.styles.createClass({
  name: 'highlighted',
  nodeAttributes: { color: 'red', radius: n => +n.getAttribute('radius') * 2 },
  edgeAttributes: { color: 'red', width: e => +e.getAttribute('width') * 2 }
});
const ANIMATION_DURATION = 250;
ogma.events.on('mouseover', ({ target }) => {
  if (!target || !target.isNode) return;
  const metaNode = target.getMetaNode();
  if (!metaNode) return;
  state.hoveredNode = target;
  state.edgesToHighlight = target.getAdjacentEdges({ filter: 'all' });
  state.edgesVisibility = state.edgesToHighlight.map(e => e.isVisible());
  state.nodesToHighlight = ogma.getNodes(
    state.edgesToHighlight
      .map(edge =>
        edge.getSource() === target ? edge.getTarget() : edge.getSource()
      )
      .filter(node => !node.isVirtual())
      .concat(target)
      .map(node => node.getId())
  );

  state.edgesToDim = ogma
    .getEdges()
    .filter(edge => !state.edgesToHighlight!.filter(e => e === edge).size);

  state.nodesToDim = ogma.getNodes(
    state.edgesToDim
      .filter(
        edge => (edge.getTarget() === target) !== (edge.getSource() === target)
      )
      .map(edge =>
        edge.getSource() === target ? edge.getTarget() : edge.getSource()
      )
      .filter(node => !node.isVirtual() && node !== target)
      .map(node => node.getId())
  );

  state.edgesToHighlight.setVisible(true);
  state.edgesToHighlight.addClass('highlighted');
  state.nodesToHighlight.addClass('highlighted');
  state.nodesToDim.addClass('dimmed', ANIMATION_DURATION);
  state.edgesToDim.addClass('dimmed', ANIMATION_DURATION);
});

ogma.events.on('mouseout', () => {
  if (!state.hoveredNode) return;
  state.nodesToDim!.removeClass('dimmed', ANIMATION_DURATION);
  state.edgesToDim!.removeClass('dimmed', ANIMATION_DURATION);
  state.edgesToHighlight!.removeClass('highlighted');
  state.nodesToHighlight!.removeClass('highlighted');
  state.edgesToHighlight!.forEach((e, i) =>
    e.setVisible(state.edgesVisibility[i])
  );

  state.edgesToHighlight = null;
  state.hoveredNode = null;
});
html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />

    <link type="text/css" rel="stylesheet" href="styles.css" />
    <link
      type="text/css"
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/solid.min.css"
    />
  </head>

  <body>
    <div id="graph-container"></div>
    <script type="module" src="index.ts"></script>
  </body>
</html>
css
#graph-container {
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  position: absolute;
  margin: 0;
  overflow: hidden;
}

button {
  position: absolute;
  top: 10px;
  left: 10px;
}

#custom-group-btn {
  top: 40px;
}