Skip to content
  1. Examples

Anti Money Laundering demo

This example shows how to use NodeGrouping and EdgeGrouping to reduce the complexity of a graph and enhance money laundering patterns.

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

/**
 * Create the chart
 */
const ogma = new Ogma({
  container: 'graph-container',
  options: {
    interactions: { zoom: { onDoubleClick: true } }
  }
});

/**
 * Styling section
 */
const placeholder = document.createElement('span');
document.body.appendChild(placeholder);
placeholder.style.visibility = 'hidden';

// helper routine to get the icon HEX code
function getIconCode(className: string) {
  placeholder.className = className;
  const code = getComputedStyle(placeholder, ':before').content;
  return code[1];
}

const personIcon = getIconCode('icon-user');
const companyIcon = getIconCode('icon-building-2');
const isShareholderType = 'IS-SHAREHOLDER';

const getNodeColor = (node: Node) => {
  const group = node.getData('group');
  if (group === 'purple') return '#67328E';
  if (group === 'green') return '#328E5B';
  return '#DE8B53';
};

const getEdgeColor = edge => {
  if (edge.getData('type') === isShareholderType) return '#89C7D6';
  return '#8E6538';
};

const getEdgeHaloColor = edge => {
  if (edge.getData('type') === isShareholderType) return '#BDF2FF';
  return '#DABB98';
};

ogma.styles.addNodeRule({
  text: {
    minVisibleSize: 0,
    font: 'IBM Plex Sans'
  },
  color: 'white',
  outerStroke: {
    color: getNodeColor
  },
  icon: {
    font: 'Lucide',
    content: node => {
      if (node.getData('type') === 'company') {
        return companyIcon;
      }
      return personIcon;
    },
    style: 'bold',
    color: getNodeColor
  }
});

ogma.styles.setHoveredNodeAttributes({
  outline: false, // Disabling the shadow on hover
  outerStroke: {
    color: getNodeColor
  },
  text: {
    tip: false
  }
});

ogma.styles.addEdgeRule({
  shape: {
    head: 'arrow'
  },
  color: getEdgeColor
});

ogma.styles.setHoveredEdgeAttributes({
  outline: true,
  color: getEdgeColor,
  halo: getEdgeHaloColor,
  text: {
    backgroundColor: 'rgb(220, 220, 220)',
    minVisibleSize: 0
  }
});

const formatContent = ids => {
  if (!Array.isArray(ids)) {
    return ''; // this is for regular nodes
  }
  return (
    '<br/><br/>Contains:<ul/><li>"' + ids.join('"</li><li/>"') + '"</li></ul>'
  );
};

const createTooltip = (title: string, content: string, color: string) =>
  `<div class="arrow"></div>
  <div class="ogma-tooltip-header">
    <span class="title">${title}<span class="line-color" style="background: ${color}"></span>
    <div>
    ${formatContent(content)}</div>
  </div>`;

// Show some info from the hovered node
ogma.tools.tooltip.onNodeHover(
  node => {
    // pick the color of the node
    const color = node.getAttribute('outerStroke');
    // Now change the title based on the type of node (grouped or not?)
    const title = node.isVirtual()
      ? 'Grouped Area&nbsp;'
      : 'ID: "' + node.getId() + '"&nbsp;';
    // and pick the content of the node if grouped;
    const content = node.isVirtual() ? node.getData('ids') : node.getId();
    return createTooltip(title, content, color);
  },
  {
    className: 'ogma-tooltip' // tooltip container class to bind to css
  }
);

const graph = await Ogma.parse.jsonFromUrl('ownerships.json');
await ogma.setGraph(graph);
await ogma.layouts.force({ locate: { padding: 120 } });

const updateUI = () => {
  // Check what it should
  const nodeGrouped = getMode('node-group') === 'group';
  const edgeGrouped = getMode('edge-group') === 'group';

  // instead of working out how to go from state A to state B
  // execute all transformation and for each one workout what to do
  execute(edgeGrouping, edgeGrouped);
  execute(nodeGrouping, nodeGrouped);
  return ogma.transformations.afterNextUpdate().then(() => {
    return ogma.layouts.force();
  });
};

// this is the duration of the animations
const transitionTime = 500;
// Menu at the top right
const form = document.querySelector('#ui form')!;
form.addEventListener('change', updateUI);

document.querySelector('#layout')!.addEventListener('click', evt => {
  evt.preventDefault();
  ogma.layouts.force();
});

// transformation objects: create them now and use them later
const nodeGrouping = ogma.transformations.addNodeGrouping({
  selector: node => node.getData('type') === 'company',
  groupIdFunction: node => node.getData('group'),
  nodeGenerator: (nodeList, groupId) => ({
    id: 'grouped-node-' + groupId,
    attributes: {
      text: groupId + ' Group',
      radius: nodeList.size * 3,
      color: nodeList.getAttribute('icon').color
    },
    // add some content to be picked up later on in the tooltip
    data: {
      group: nodeList.getData('group')[0],
      type: nodeList.getData('type')[0],
      ids: nodeList.getId()
    }
  }),
  groupEdges: false, // do not group edges as for now
  enabled: false // create the transformation but do not activate it
});
const edgeGrouping = ogma.transformations.addEdgeGrouping({
  generator: (edgeList, groupId) => {
    const data = edgeList.getData()[0];
    return {
      id: groupId,
      data: data,
      attributes: {
        width: edgeList.size
      }
    };
  },
  enabled: false // create the transformation but do not activate it
});

const execute = (transformation, enable) => {
  // avoid extra work if we're already in the state requested
  if (transformation.isEnabled() === enable) {
    return;
  }
  transformation.toggle(transitionTime);
};

const getMode = id => {
  const select = form[id];
  const currentMode = Array.prototype.filter.call(select, input => {
    return input.checked;
  })[0].value; // IE inconsistency
  return currentMode;
};
html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />

    <link type="text/css" rel="stylesheet" href="style.css" />
    <link
      href="https://cdn.jsdelivr.net/npm/lucide-static@0.483.0/font/lucide.css"
      rel="stylesheet"
    />
    <link
      href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap"
      rel="stylesheet"
    />
  </head>

  <body>
    <div id="graph-container"></div>
    <div class="toolbar" id="ui">
      <form>
        <div class="section mode">
          <h3>Company analysis</h3>
          <div class="switch switch--horizontal">
            <input
              type="radio"
              name="node-group"
              value="detail"
              checked="checked"
            />
            <label for="detail">All Nodes</label>
            <input type="radio" name="node-group" value="group" />
            <label for="group">Group</label>
            <span class="toggle-outside">
              <span class="toggle-inside"></span>
            </span>
          </div>
          <div class="switch switch--horizontal">
            <input
              type="radio"
              name="edge-group"
              value="detail"
              checked="checked"
            />
            <label for="detail">All Edges</label>
            <input type="radio" name="edge-group" value="group" />
            <label for="group">Backbone</label>
            <span class="toggle-outside">
              <span class="toggle-inside"></span>
            </span>
          </div>
        </div>
        <div class="controls">
          <button id="layout" class="btn menu">Layout</button>
        </div>
      </form>
      <div class="section mode" id="details"></div>
    </div>
    <script src="index.ts"></script>
  </body>
</html>
css
:root {
  --font: 'IBM Plex Sans', sans-serif;
}

body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  font-family: var(--font);
}

*,
*:before,
*:after {
  box-sizing: border-box;
}

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

.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;
}

.toolbar .section {
  position: relative;
  display: block;
}

.toolbar .section h3 {
  display: block;
  font-weight: 300;
  border-bottom: 1px solid #ddd;
  color: #606060;
  font-size: 1rem;
}

.toolbar .section .clearable-input {
  border-radius: 4px;
  padding: 5px;
  border: 1px solid #dddddd;
}

.toolbar .section .line-color,
.ogma-tooltip .line-color {
  display: inline-block;
  width: 1rem;
  height: 1rem;
  margin-right: 0.25rem;
  border-radius: 50%;
  color: #ffffff;
  text-align: center;
  font-size: 0.65rem;
  line-height: 1rem;
  font-weight: bold;
  vertical-align: text-top;
}

.toolbar .section p {
  padding-left: 18px;
  padding-right: 18px;
}

.toolbar .section p label {
  width: 4rem;
  display: inline-block;
}

.toolbar .mode {
  text-align: center;
}

.toolbar .disabled {
  display: none;
}

.ogma-tooltip-header .title {
  font-weight: bold;
  text-transform: uppercase;
}

/* --- Tooltip */
.ogma-tooltip {
  max-width: 240px;
  max-height: 280px;
  background-color: #fff;
  border: 1px solid #999;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
  border-radius: 6px;
  cursor: auto;
  font-size: 12px;
  pointer-events: none;
}

.ogma-tooltip-header {
  font-variant: small-caps;
  font-size: 120%;
  color: #000;
  border-bottom: 1px solid #999;
  padding: 10px;
}

.ogma-tooltip-body {
  padding: 10px;
  overflow-x: hidden;
  overflow-y: auto;
  max-width: inherit;
  max-height: 180px;
}

.ogma-tooltip-body th {
  color: #999;
  text-align: left;
}

.ogma-tooltip-footer {
  padding: 10px;
  border-top: 1px solid #999;
}

.ogma-tooltip > .arrow {
  border-width: 10px;
  position: absolute;
  display: block;
  width: 0;
  height: 0;
  border-color: transparent;
  border-style: solid;
}

.ogma-tooltip.top {
  margin-top: -12px;
}

.ogma-tooltip.top > .arrow {
  left: 50%;
  bottom: -10px;
  margin-left: -10px;
  border-top-color: #999;
  border-bottom-width: 0;
}

.ogma-tooltip.bottom {
  margin-top: 12px;
}

.ogma-tooltip.bottom > .arrow {
  left: 50%;
  top: -10px;
  margin-left: -10px;
  border-bottom-color: #999;
  border-top-width: 0;
}

.ogma-tooltip.left {
  margin-left: -12px;
}

.ogma-tooltip.left > .arrow {
  top: 50%;
  right: -10px;
  margin-top: -10px;
  border-left-color: #999;
  border-right-width: 0;
}

.ogma-tooltip.right {
  margin-left: 12px;
}

.ogma-tooltip.right > .arrow {
  top: 50%;
  left: -10px;
  margin-top: -10px;
  border-right-color: #999;
  border-left-width: 0;
}

/* -- CSS flip switch */
.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: 100%;
  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: 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;
}

.hidden {
  display: none;
}

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

.control-bar {
  font-family: Helvetica, Arial, sans-serif;
  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65);
  border-radius: 4px;
}

.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;
}
json
{
  "nodes": [
    {
      "id": "owner-1",
      "attributes": {
        "radius": 10,
        "text": "Stakeholder #1"
      },
      "data": {
        "group": "purple",
        "type": "person"
      }
    },
    {
      "id": "company-1",
      "attributes": {
        "radius": 10,
        "text": "Company #1"
      },
      "data": {
        "group": "purple",
        "type": "company"
      }
    },
    {
      "id": "company-2",
      "attributes": {
        "radius": 8,
        "text": "Company #2"
      },
      "data": {
        "group": "purple",
        "type": "company"
      }
    },
    {
      "id": "owner-2",
      "attributes": {
        "radius": 10,
        "text": "Stakeholder #2"
      },
      "data": {
        "group": "green",
        "type": "person"
      }
    },
    {
      "id": "company-3",
      "attributes": {
        "radius": 10,
        "text": "Company #3"
      },
      "data": {
        "group": "green",
        "type": "company"
      }
    },
    {
      "id": "company-4",
      "attributes": {
        "radius": 8,
        "text": "Company #4"
      },
      "data": {
        "group": "green",
        "type": "company"
      }
    },
    {
      "id": "owner-3",
      "attributes": {
        "radius": 10,
        "text": "Stakeholder #3"
      },
      "data": {
        "group": "orange",
        "type": "person"
      }
    },
    {
      "id": "company-5",
      "attributes": {
        "radius": 10,
        "text": "Company #5"
      },
      "data": {
        "group": "orange",
        "type": "company"
      }
    },
    {
      "id": "company-6",
      "attributes": {
        "radius": 8,
        "text": "Company #6"
      },
      "data": {
        "group": "orange",
        "type": "company"
      }
    },
    {
      "id": "company-7",
      "attributes": {
        "radius": 8,
        "text": "Company #7"
      },
      "data": {
        "group": "orange",
        "type": "company"
      }
    },
    {
      "id": "company-8",
      "attributes": {
        "radius": 8,
        "text": "Company #8"
      },
      "data": {
        "group": "orange",
        "type": "company"
      }
    }
  ],
  "edges": [
    {
      "id": "is-shareholder-0",
      "source": "owner-1",
      "target": "company-1",
      "attributes": {
        "text": "IS-SHAREHOLDER"
      },
      "data": {
        "type": "IS-SHAREHOLDER"
      }
    },
    {
      "id": "is-shareholder-1",
      "source": "company-1",
      "target": "company-2",
      "attributes": {
        "text": "IS-SHAREHOLDER"
      },
      "data": {
        "type": "IS-SHAREHOLDER"
      }
    },
    {
      "id": "is-shareholder-2",
      "source": "owner-2",
      "target": "company-4",
      "attributes": {
        "text": "IS-SHAREHOLDER"
      },
      "data": {
        "type": "IS-SHAREHOLDER"
      }
    },
    {
      "id": "is-shareholder-3",
      "source": "owner-2",
      "target": "company-3",
      "attributes": {
        "text": "IS-SHAREHOLDER"
      },
      "data": {
        "type": "IS-SHAREHOLDER"
      }
    },
    {
      "id": "is-shareholder-4",
      "source": "company-4",
      "target": "company-3",
      "attributes": {
        "text": "IS-SHAREHOLDER"
      },
      "data": {
        "type": "IS-SHAREHOLDER"
      }
    },
    {
      "id": "is-shareholder-5",
      "source": "owner-3",
      "target": "company-5",
      "attributes": {
        "text": "IS-SHAREHOLDER"
      },
      "data": {
        "type": "IS-SHAREHOLDER"
      }
    },
    {
      "id": "is-shareholder-6",
      "source": "owner-3",
      "target": "company-6",
      "attributes": {
        "text": "IS-SHAREHOLDER"
      },
      "data": {
        "type": "IS-SHAREHOLDER"
      }
    },
    {
      "id": "is-shareholder-7",
      "source": "company-6",
      "target": "company-8",
      "attributes": {
        "text": "IS-SHAREHOLDER"
      },
      "data": {
        "type": "IS-SHAREHOLDER"
      }
    },
    {
      "id": "is-shareholder-8",
      "source": "company-5",
      "target": "company-7",
      "attributes": {
        "text": "IS-SHAREHOLDER"
      },
      "data": {
        "type": "IS-SHAREHOLDER"
      }
    },
    {
      "id": "is-shareholder-9",
      "source": "company-7",
      "target": "company-8",
      "attributes": {
        "text": "IS-SHAREHOLDER"
      },
      "data": {
        "type": "IS-SHAREHOLDER"
      }
    },
    {
      "id": "transaction-1",
      "source": "company-5",
      "target": "company-4",
      "attributes": {
        "text": "IS-TRANSACTION"
      },
      "data": {
        "type": "IS-TRANSACTION"
      }
    },
    {
      "id": "transaction-2",
      "source": "company-7",
      "target": "company-4",
      "attributes": {
        "text": "IS-TRANSACTION"
      },
      "data": {
        "type": "IS-TRANSACTION"
      }
    },
    {
      "id": "transaction-3",
      "source": "company-8",
      "target": "company-3",
      "attributes": {
        "text": "IS-TRANSACTION"
      },
      "data": {
        "type": "IS-TRANSACTION"
      }
    },
    {
      "id": "transaction-4"

...