Appearance
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;