Skip to content
  1. Examples

Fraud detection

This example shows how to use Ogma to create a fraud detection system.

js
import Ogma from '@linkurious/ogma';

// Retrieve the fake database defined in `dummyDatabase.js`
import * as DB from './dummyDatabase';

// Constants used to configure the camera and the layouts
const LAYOUT_DURATION = 400;
const LOCATE_OPTIONS = {
  duration: 400,
  padding: { top: 200, bottom: 80, left: 50, right: 50 }
};

// Create an instance of Ogma
// We know we are dealing with a small graph, so we can afford to use the canvas renderer
const ogma = new Ogma({ renderer: 'canvas' });

// Retrieve the important elements of the UI
const searchBar = document.getElementById('search-bar');
const boxHideEvaluators = document.getElementById('box-hide-evaluators');
const boxHideSmallClaims = document.getElementById('box-hide-small-claims');
const boxHideLeaves = document.getElementById('box-hide-leaves');
const boxTexts = document.getElementById('box-assign-texts');
const boxIcons = document.getElementById('box-assign-icons');
const boxColors = document.getElementById('box-assign-colors');
const boxClaims = document.getElementById('box-assign-claim-sizes');
const boxShowLegend = document.getElementById('box-show-legend');
const buttonForceLink = document.getElementById('btn-forceLink');
const buttonHierarchical = document.getElementById('btn-hierarchical');
const buttonSearch = document.getElementById('btn-search');
const buttonExport = document.getElementById('btn-export');
const buttonCenter = document.getElementById('btn-center');
const buttonZoomOut = document.getElementById('btn-zoom-out');
const buttonZoomIn = document.getElementById('btn-zoom-in');
const buttonReset = document.getElementById('btn-reset');
const buttonDisplayAll = document.getElementById('btn-display-all');

// Initialize the style rules/filters
let textRule = null;
let iconRule = null;
let colorRule = null;
let sizeRule = null;
let evaluatorFilter = null;
let smallClaimFilter = null;
let leafFilter = null;

// Main function, initializes the visualization
const init = () => {
  // Assign the HTML container to Ogma, and set the background color to be a bit darker that white
  ogma.setContainer('graph-container');
  ogma.setOptions({ backgroundColor: '#F0F0F0' });

  // Bind the buttons/checkboxes to the associated actions
  boxHideEvaluators.addEventListener('click', applyEvaluatorFilter);
  boxHideSmallClaims.addEventListener('click', applySmallClaimFilter);
  boxHideLeaves.addEventListener('click', applyLeafFilter);
  boxTexts.addEventListener('click', applyTextRule);
  boxColors.addEventListener('click', applyColorRule);
  boxIcons.addEventListener('click', applyIconRule);
  boxClaims.addEventListener('click', applyClaimSizeRule);
  boxShowLegend.addEventListener('click', toggleLegend);
  buttonForceLink.addEventListener('click', runForceLayout);
  buttonHierarchical.addEventListener('click', runHierarchical);
  buttonZoomIn.addEventListener('click', zoomIn);
  buttonZoomOut.addEventListener('click', zoomOut);
  buttonCenter.addEventListener('click', centerView);
  buttonSearch.addEventListener('click', searchNode);
  buttonExport.addEventListener('click', exportToPng);
  buttonReset.addEventListener('click', reset);
  buttonDisplayAll.addEventListener('click', displayAll);

  // Trigger the search on "Enter" when search bar has focus
  document.addEventListener('keydown', evt => {
    const code = evt.keyCode || evt.which;
    if (code === 13 && document.activeElement === searchBar) {
      searchNode();
    }
  });

  // Expand a node when double-clicked
  ogma.events.on('doubleclick', evt => {
    if (evt.target && evt.target.isNode && evt.button === 'left') {
      expandNeighbors(evt.target);

      // Clicking on a node adds it to the selection, but we don't want a node to
      // be selected when double-clicked
      evt.target.setSelected(!evt.target.isSelected());
    }
  });

  // Styling rules that will be applied globally
  ogma.styles.addNodeRule({
    text: {
      size: 14
    },
    icon: {
      font: 'Font Awesome 5 Free',
      style: 'bold'
    },
    badges: {
      bottomRight: {
        color: 'inherit',
        text: {
          scale: 0.8,
          content: (
            node // The bottom right badge displays the number of hidden neighbors
          ) =>
            // The `nbNeighbors` data property contains the total number of neighbors for the node in the DB
            // The `getDegree` method retrieves the number of neighbors displayed in the viz
            node.getData('nbNeighbors') - node.getDegree() || null
        }
      }
    }
  });

  ogma.styles.addEdgeRule({
    shape: 'arrow'
  });

  // Show the type of the edges on hover
  ogma.styles.setHoveredEdgeAttributes({
    text: node => node.getData('type')
  });

  // Display the data of nodes as a tooltip on hover
  ogma.tools.tooltip.onNodeHover(
    node => {
      const html = [
        '<div class="arrow"></div>',
        '<div class="ogma-tooltip-header">' + node.getData('type') + '</div>',
        '<div class="ogma-tooltip-body">',
        '<table>'
      ];

      const properties = node.getData('properties');

      Object.keys(properties).forEach(key => {
        const value = properties[key];

        html.push('<tr><th>' + key + '</th><td>' + value + '</td></tr>');
      });

      html.push(
        '</table></div><div class="ogma-tooltip-footer">Number of connections: ' +
          node.getData('nbNeighbors') +
          '</div>'
      );

      return html.join('');
    },
    { className: 'ogma-tooltip' }
  );

  // Set the focus on the search bar
  searchBar.focus();

  // Set the default value for the search bar
  //searchBar.value = 'John Piggyback';

  // Set the cursor at the end of the search bar
  searchBar.setSelectionRange(searchBar.value.length, searchBar.value.length);

  boxTexts.checked = true;
  boxColors.checked = true;
  boxIcons.checked = true;

  // Apply the text, color and icon rules
  applyTextRule();
  applyColorRule();
  applyIconRule();
};

// Add the given node to the visualization and returns the added node if it exist
const addNodeToViz = name => {
  // Look for the node in the DB
  const node = DB.search(name);

  if (node) {
    const addedNode = ogma.addNode(node);

    if (ogma.getNodes().size === 1) {
      // If this is the first node in the visualization, we simply center the camera on it
      ogma.view.locateGraph();
    } else {
      // If there are more than one node, apply the force layout (force-directed)
      ogma
        .addEdges(selectAdjacentEdgesToAdd([node.id]))
        .then(() => runForceLayout());
    }

    return addedNode;
  } else {
    return null;
  }
};

// Retrieve the list of adjacent edges to the specified nodes, for which both the
// source and target are already loaded in Ogma (in the viz)
const selectAdjacentEdgesToAdd = nodeIds =>
  DB.getAdjacentEdges(nodeIds).filter(edge => {
    return ogma.getNode(edge.source) && ogma.getNode(edge.target);
  });

// Expand the specified node by retrieving its neighbors from the database
// and adding them to the visualization
const expandNeighbors = node => {
  // Retrieve the neighbors from the DB
  const neighbors = DB.getNeighbors(node.getId()),
    ids = neighbors.nodeIds,
    nodes = neighbors.nodes;

  // If this condition is false, it means that all the retrieved nodes are already in Ogma.
  // In this case we do nothing
  if (ogma.getNodes(ids).size < ids.length) {
    // Set the position of the neighbors around the nodes, in preparation to the force-directed layout
    let position = node.getPosition(),
      angleStep = (2 * Math.PI) / neighbors.nodes.length,
      angle = Math.random() * angleStep;

    for (let i = 0; i < nodes.length; ++i) {
      const neighbor = nodes[i];
      neighbor.attributes = {
        x: position.x + Math.cos(angle) * 0.001,
        y: position.y + Math.sin(angle) * 0.001
      };

      angle += angleStep;
    }

    // Add the neighbors to the visualization, add their adjacent edges and run a layout
    return ogma
      .addNodes(nodes)
      .then(() => ogma.addEdges(selectAdjacentEdgesToAdd(ids)))
      .then(() => runForceLayout());
  }
};

/* ============================== */
/* Function triggered by the menu */
/* ============================== */

const applyEvaluatorFilter = () => {
  if (boxHideEvaluators.checked && !evaluatorFilter) {
    evaluatorFilter = ogma.transformations.addNodeFilter(node => {
      return node.getData('type') !== 'Evaluator';
    });
  } else if (!boxHideEvaluators.checked && evaluatorFilter) {
    evaluatorFilter.destroy();
    evaluatorFilter = null;
  }
};

const applySmallClaimFilter = () => {
  if (boxHideSmallClaims.checked && !smallClaimFilter) {
    smallClaimFilter = ogma.transformations.addNodeFilter(node => {
      return (
        node.getData('type') !== 'Claim' ||
        node.getData('properties.amount') >= 50000
      );
    });
  } else if (!boxHideSmallClaims.checked && smallClaimFilter) {
    smallClaimFilter.destroy();
    smallClaimFilter = null;
  }
};

const applyLeafFilter = () => {
  if (boxHideLeaves.checked && !leafFilter) {
    leafFilter = ogma.transformations.addNodeFilter(node => {
      return node.getAdjacentNodes().size > 1;
    });
  } else if (!boxHideLeaves.checked && leafFilter) {
    leafFilter.destroy();
    leafFilter = null;
  }
};

const applyTextRule = () => {
  if (boxTexts.checked && !textRule) {
    textRule = ogma.styles.addNodeRule({
      text: {
        content: node => {
          const type = node.getData('type');

          if (
            type === 'Customer' ||
            type === 'Lawyer' ||
            type === 'Evaluator'
          ) {
            return node.getData('properties.fullname');
          } else if (
            type === 'Phone' ||
            type === 'MailAddress' ||
            type === 'SSN'
          ) {
            return node.getData('properties.name');
          } else if (type === 'Address') {
            return (
              node.getData('properties.city') +
              ', ' +
              node.getData('properties.state')
            );
          } else if (type === 'Claim') {
            return (
              node.getData('properties.name') +
              ' (' +
              node.getData('properties.amount') +
              '$)'
            );
          }
        }
      }
    });
  } else if (!boxTexts.checked && textRule) {
    textRule.destroy();
    textRule = null;
  }
};

const applyColorRule = () => {
  if (boxColors.checked && !colorRule) {
    colorRule = ogma.styles.addNodeRule({
      color: ogma.rules.map({
        field: 'type',
        values: {
          Customer: '#FF5217',
          Claim: '#E8AB15',
          Address: '#D0FF24',
          SSN: '#0FE85C',
          Phone: '#0EB1FF',
          MailAddress: '#FF1271',
          Lawyer: '#AE15FF',
          Evaluator: '#1067FF'
        }
      })
    });
  } else if (!boxColors.checked && colorRule) {
    colorRule.destroy();
    colorRule = null;
  }
};

const applyIconRule = () => {
  if (boxIcons.checked && !iconRule) {
    iconRule = ogma.styles.addNodeRule({
      icon: {
        content: ogma.rules.map({
          field: 'type',
          values: {
            Customer: '\uf007',
            Claim: '\uf155',
            Address: '\uf015',
            SSN: '\uf2c2',
            Phone: '\uf095',
            MailAddress: '\uf1fa',
            Lawyer: '\uf0e3',
            Evaluator: '\uf508'
          }
        })
      }
    });
  } else if (!boxIcons.checked && iconRule) {
    iconRule.destroy();
    iconRule = null;
  }
};

const applyClaimSizeRule = () => {
  if (boxClaims.checked && !sizeRule) {
    sizeRule = ogma.styles.addNodeRule(
      node => {
        return node.getData('type') === 'Claim';
      },
      {
        radius: ogma.rules.slices({
          field: 'properties.amount',
          values: { min: 3, max: 7 },
          stops: { min: 48000, max: 52000 }
        })
      }
    );
  } else if (!boxClaims.checked && sizeRule) {
    sizeRule.destroy();
    sizeRule = null;
  }
};

const toggleLegend = () => {
  if (boxShowLegend.checked) {
    ogma.tools.legend.enable({
      fontSize: 11,
      titleFunction: propertyPath => {
        const propName = propertyPath[propertyPath.length - 1];

        if (propName === 'amount') {
          return 'Claim amount';
        } else {
          return propName.charAt(0).toUpperCase() + propName.substring(1);
        }
      }
    });
  } else {
    ogma.tools.legend.disable();
  }
};

// Utility function to run a layout
const runLayout = name =>
  ogma.layouts[name]({
    locate: LOCATE_OPTIONS,
    duration: LAYOUT_DURATION
  });

const runForceLayout = () => runLayout('force');
const runHierarchical = () => runLayout('hierarchical');

const displayAll = () => {
  // Retrieve the whole graph from the database
  const graph = DB.getFullGraph();

  // If there is the same amount of nodes in Ogma as in the DB, nothing to do
  if (ogma.getNodes('all').size !== graph.nodes.length) {
    // Assign a random position to all nodes, in preparation for the layout
    for (let i = 0; i < graph.nodes.length; ++i) {
      graph.nodes[i].attributes = {
        x: Math.random(),
        y: Math.random()
      };
    }

    ogma
      .setGraph(graph)
      .then(() => ogma.view.locateGraph(LOCATE_OPTIONS))
      .then(() => runForceLayout());
  }
};

const reset = () => {
  ogma.clearGraph();
  boxHideEvaluators.checked = false;
  boxHideSmallClaims.checked = false;
  boxHideLeaves.checked = false;
  boxTexts.checked = true;
  boxColors.checked = true;
  boxIcons.checked = true;
  boxClaims.checked = false;
  boxShowLegend.checked = false;

  applyTextRule();
  applyLeafFilter();
  applySmallClaimFilter();
  applyEvaluatorFilter();
  applyClaimSizeRule();
  applyColorRule();
  applyIconRule();
  toggleLegend();
};

/* ================================================== */
/* Function triggered by buttons in the visualization */
/* ================================================== */

const zoomIn = () => {
  ogma.view.zoomIn({
    duration: 150,
    easing: 'quadraticOut'
  });
};

const zoomOut = () => {
  ogma.view.zoomOut({
    duration: 150,
    easing: 'quadraticOut'
  });
};

const searchNode = () => {
  if (searchBar.value) {
    if (addNodeToViz(searchBar.value)) {
      searchBar.value = '';
    } else {
      alert(
        'No node has the property "fullname" equal to "' +
          searchBar.value +
          '".'
      );
    }
  }
};

const centerView = () => ogma.view.locateGraph(LOCATE_OPTIONS);

const exportToPng = () => {
  ogma.export.png({
    background: '#F0F0F0',
    filename: 'Linkurious_Ogma_Library_Example'
  });
};

init();
searchNode();

searchBar.value = 'Patrick Collison';
html
<html>
  <head>
    <link
      rel="stylesheet"
      type="text/css"
      href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.min.css"
    />
    <link type="text/css" rel="stylesheet" href="style.css" />
  </head>
  <body>
    <div>
      <div
        style="
          top: 0;
          bottom: 0;
          left: 0;
          right: 0;
          position: absolute;
          margin: 0;
        "
      >
        <div style="position: relative; height: 100%; font-size: 18px">
          <div
            style="
              position: relative;
              max-height: 100%;
              overflow-y: auto;
              padding-left: 10px;
              width: 250px;
              float: left;
              font-size: 16px;
            "
          >
            <div style="padding: 15px 0 10px 0; text-align: center">
              Use the menu options to apply filters, styles and layouts.
            </div>
            <hr style="width: 80%" />
            <h2>Filters</h2>
            <div>
              <input
                id="box-hide-evaluators"
                type="checkbox"
                title='Hide nodes for which the property "type" is "Evaluator"'
              />
              Hide evaluators
              <br />
              <input
                id="box-hide-small-claims"
                type="checkbox"
                title='Hide nodes of type "Claim" for which the property "properties.amount" is less than 50000'
              />
              Hide claims < 50K$
              <br />
              <input
                id="box-hide-leaves"
                type="checkbox"
                title="Hide nodes that have less than two neighbors"
              />
              Hide leaf nodes
            </div>
            <h2>Styles</h2>
            <div>
              <input
                id="box-assign-texts"
                type="checkbox"
                title='Assign node texts based on the "type" property'
              />
              Node text
              <br />
              <input
                id="box-assign-colors"
                type="checkbox"
                title='Assign node colors based on the "type" property'
              />
              Node color
              <br />
              <input
                id="box-assign-icons"
                type="checkbox"
                title='Assign node icons based on the "type" property'
              />
              Node icon
              <br />
              <input
                id="box-assign-claim-sizes"
                type="checkbox"
                title='Assign the size of nodes of type "Claim" based on their "amount" property'
              />
              Claim size
              <br />
              <input
                id="box-show-legend"
                type="checkbox"
                title="Show the legend"
              />
              Legend
            </div>
            <h2>Layouts</h2>
            <div style="text-align: center">
              <button id="btn-forceLink" class="btn menu">ForceLink</button>
              <button id="btn-hierarchical" class="btn menu">
                Hierarchical
              </button>
            </div>
            <h2>Actions</h2>
            <div
              style="
                text-align: center;
                font-family: OpenSansLight, monospace;
                margin-top: 10px;
              "
            >
              <button id="btn-display-all" class="btn menu">Display all</button>
              <button id="btn-reset" class="btn menu">Reset</button>
            </div>
          </div>
          <div
            id="graph-container"
            style="
              position: absolute;
              right: 0;
              top: 0;
              left: 260px;
              bottom: 0;
              height: 100%;
              width: calc(100% - 260px);
            "
          >
            <div
              style="
                position: absolute;
                top: 35px;
                text-align: center;
                width: 100%;
              "
            >
              <input
                id="search-bar"
                type="text"
                placeholder="Enter a name"
                value="John Piggyback"
                style="
                  width: 300px;
                  font-size: 16px;
                  line-height: 16px;
                  outline: none;
                  border-radius: 15px;
                  padding: 5px 20px;
                  border: none;
                "
              />
              <button
                id="btn-search"
                title="Search"
                class="btn"
                style="
                  border-radius: 15px;
                  margin-left: -30px;
                  padding: 4px 8px 6px;
                "
              >
                <i class="fa fa-search" aria-hidden="true"></i>
              </button>
            </div>
            <div style="position: absolute; right: 20px; top: 35px">
              <button
                id="btn-export"
                title="Export the current view as a PNG"
                class="btn"
              >
                <i class="fa fa-download" aria-hidden="true"></i>
              </button>
            </div>
            <div style="position: absolute; right: 20px; bottom: 20px">
              <button
                id="btn-center"
                title="Center the view on the graph"
                class="btn"
                style="margin-right: 30px"
              >
                <i class="fa fa-crosshairs" aria-hidden="true"></i>
              </button>
              <button id="btn-zoom-out" title="Zoom out" class="btn">
                <i class="fa fa-minus" aria-hidden="true"></i>
              </button>
              <button id="btn-zoom-in" title="Zoom in" class="btn">
                <i class="fa fa-plus" aria-hidden="true"></i>
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
    <script src="index.js"></script>
  </body>
</html>
css
@font-face {
  font-family: OpenSansRegular;
  src: url("../fonts/OpenSans/OpenSans-Regular.ttf");
}

@font-face {
  font-family: OpenSansLight;
  src: url("../fonts/OpenSans/OpenSans-Light.ttf");
}

body {
  font-family: OpenSansRegular, sans-serif;
}

h2 {
  font-size: 16px;
  font-family: OpenSansLight, sans-serif;
}

.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 black;
  width: 80%;
  font-size: 14px;
  margin-top: 10px;
}

canvas {
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -o-user-select: none;
  user-select: none;
}

.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-family: Arial, sans-serif;
  font-size: 12px;
}
.ogma-tooltip .icon {
  font-family: FontAwesome, sans-serif;
}
.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: 10px transparent solid;
  position: absolute;
  display: block;
  width: 0;
  height: 0;
}
.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;
}
json
{
  "nodes": [
    {
      "id": 0,
      "data": {
        "type": "Customer",
        "properties": {
          "country": "USA",
          "fullname": "John Piggyback",
          "age": 32
        },
        "nbNeighbors": 6
      },
      "neighbors": {
        "edges": [
          0,
          1,
          2,
          3,
          4,
          5
        ],
        "nodes": [
          4,
          2,
          3,
          1,
          6,
          5
        ]
      }
    },
    {
      "id": 1,
      "data": {
        "type": "Phone",
        "properties": {
          "name": "123-878-000"
        },
        "nbNeighbors": 2
      },
      "neighbors": {
        "edges": [
          3,
          18
        ],
        "nodes": [
          0,
          15
        ]
      }
    },
    {
      "id": 2,
      "data": {
        "type": "SSN",
        "properties": {
          "name": 985365741
        },
        "nbNeighbors": 1
      },
      "neighbors": {
        "edges": [
          1
        ],
        "nodes": [
          0
        ]
      }
    },
    {
      "id": 3,
      "data": {
        "type": "Address",
        "properties": {
          "city": "Key West",
          "street": "Eisenhower Street",
          "state": "Florida"
        },
        "nbNeighbors": 1
      },
      "neighbors": {
        "edges": [
          2
        ],
        "nodes": [
          0
        ]
      }
    },
    {
      "id": 4,
      "data": {
        "type": "MailAddress",
        "properties": {
          "name": "john.piggyback@gmail.com"
        },
        "nbNeighbors": 1
      },
      "neighbors": {
        "edges": [
          0
        ],
        "nodes": [
          0
        ]
      }
    },
    {
      "id": 5,
      "data": {
        "type": "Claim",
        "properties": {
          "amount": 51000,
          "name": "Property damage"
        },
        "nbNeighbors": 3
      },
      "neighbors": {
        "edges": [
          5,
          6,
          8
        ],
        "nodes": [
          0,
          8,
          7
        ]
      }
    },
    {
      "id": 6,
      "data": {
        "type": "Claim",
        "properties": {
          "amount": 49000,
          "name": "Property Damage"
        },
        "nbNeighbors": 3
      },
      "neighbors": {
        "edges": [
          4,
          7,
          9
        ],
        "nodes": [
          0,
          8,
          7
        ]
      }
    },
    {
      "id": 7,
      "data": {
        "type": "Lawyer",
        "properties": {
          "fullname": "Keeley Bins"
        },
        "nbNeighbors": 3
      },
      "neighbors": {
        "edges": [
          8,
          9,
          10
        ],
        "nodes": [
          5,
          6,
          9
        ]
      }
    },
    {
      "id": 8,
      "data": {
        "type": "Evaluator",
        "properties": {
          "fullname": "Patrick Collison"
        },
        "nbNeighbors": 3
      },
      "neighbors": {
        "edges": [
          6,
          7,
          11
        ],
        "nodes": [
          5,
          6,
          9
        ]
      }
    },
    {
      "id": 9,
      "data": {
        "type": "Claim",
        "properties": {
          "amount": 50999,
          "name": "Property damage"
        },
        "nbNeighbors": 3
      },
      "neighbors": {
        "edges": [
          10,
          11,
          12
        ],
        "nodes": [
          7,
          8,
          10
        ]
      }
    },
    {
      "id": 10,
      "data": {
        "type": "Customer",
        "properties": {
          "fullname": "Werner Stiedemann"
        },
        "nbNeighbors": 5
      },
      "neighbors": {
        "edges": [
          12,
          13,
          14,
          15,
          16
        ],
        "nodes": [
          9,
          11,
          12,
          13,
          14
        ]
      }
    },
    {
      "id": 11,
      "data": {
        "type": "Address",
        "properties": {
          "city": "Alexanemouth",
          "street": "Wuckert Curve",
          "state": "Delaware"
        },
        "nbNeighbors": 1
      },
      "neighbors": {
        "edges": [
          13
        ],
        "nodes": [
          10
        ]
      }
    },
    {
      "id": 12,
      "data": {
        "type": "MailAddress",
        "properties": {
          "name": "soluta@hotmail.com"
        },
        "nbNeighbors": 2
      },
      "neighbors": {
        "edges": [
          14,
          17
        ],
        "nodes": [
          10,
          15
        ]
      }
    },
    {
      "id": 13,
      "data": {
        "type": "Phone",
        "properties": {
          "name": "485-256-662"
        },
        "nbNeighbors": 1
      },
      "neighbors": {
        "edges": [
          15
        ],
        "nodes": [
          10
        ]
      }
    },
    {
      "id": 14,
      "data": {
        "type": "SSN",
        "properties": {
          "name": 196546546
        },
        "nbNeighbors": 1
      },
      "neighbors": {
        "edges": [
          16
        ],
        "n

...
js
import GRAPH from './data.json';

// Search a node by its `fullname` property
export const search = name =>
  GRAPH.nodes.filter(node => {
    const fullname = node.data.properties.fullname;

    return fullname && fullname.toLowerCase().indexOf(name.toLowerCase()) === 0;
  })[0] || null;

// Retrieve the list of neighbors of a node
export const getNeighbors = id => {
  const neighborsIds = GRAPH.nodes[id].neighbors;

  return {
    nodeIds: neighborsIds.nodes,
    nodes: neighborsIds.nodes.map(nid => {
      return GRAPH.nodes[nid];
    })
  };
};

// Retrieve the list of adjacent edges of a list of nodes
export const getAdjacentEdges = ids => {
  const edges = [];

  GRAPH.edges.forEach(edge => {
    if (ids.indexOf(edge.source) !== -1 || ids.indexOf(edge.target) !== -1) {
      edges.push(edge);
    }
  });

  return edges;
};

// Returns the whole graph
export const getFullGraph = () => GRAPH;