This demo illustrates the use of KeyLines to discover patterns of fraud in typical insurance industry data.
The data-set represents the relations between typical actors in the car insurance domain. For example, vehicles, drivers and addresses all associated with a claim file. The demo emulates the workflow that might be offered in a real application.
After Claim 1 or Claim 2 is loaded, the user is offered the ability to 'Find Matches' between the entities shown and other data (for example, previously processed claims). The new data is then shown alongside the current claim's data. The demo then offers a 'Combine Matches' button that uses KeyLines' Combo feature to clearly expose the common entities.
While there is a common factor in the matched Claim 1 data ('Valois House'), this looks innocent as the incidents are over three years apart, and the Claim 2019-11-24 could be approved.
Claim 2's data matches show suspicious patterns related to vehicles, people and addresses, and this claim should probably be referred for further investigation.
In both cases the red Claim icon features a context menu option to select a further action.
The data set is fictional, but contains patterns typical of real fraud. Any resemblance to persons alive or dead is purely coincidental!
Key functions used:
import KeyLines from "keylines";
import data from "./data.js";
let chart;
let comboIds = [];
let matchedGroupsOfIds = [];
const defaultStyling = {
iconColours: {
address: "#A42768",
person: "#A674BA",
accident: "#7D7D7D",
vehicle: "#7FCB68",
doctor: "#1F78B4",
},
claimColour: "#E34A0E",
nodeColour: "#FFFFFF",
typeIcons: {
person: "fas fa-user",
address: "fas fa-home",
accident: "fas fa-file-invoice-dollar",
vehicle: "fas fa-car",
doctor: "fas fa-user-md",
},
linkLabel: {
padding: "3 6 0 6",
border: {
radius: "6 0 6 0",
width: 2,
colour: "grey",
},
},
};
const layoutOptions = {
tightness: 0,
stretch: 0.6,
};
let menuGlyphIsHighlighted = false;
const highlightedMenuGlyph = {
fi: {
t: "fas fa-ellipsis-v",
c: "#E30000",
},
p: 55,
r: 43,
c: "rgba(0,0,0,0)",
b: "rgba(0,0,0,0)",
e: 3.25,
};
const menuGlyph = {
fi: {
t: "fas fa-ellipsis-v",
c: defaultStyling.claimColour,
},
p: 55,
r: 43,
c: "rgba(0,0,0,0)",
b: "rgba(0,0,0,0)",
e: 2.75,
};
const claimList = document.getElementById("claimList");
const findMatchesButton = document.getElementById("findMatchesButton");
const combineButton = document.getElementById("combineButton");
const uncombineButton = document.getElementById("uncombineButton");
const contextMenu = document.getElementById("contextMenu");
const acceptClaimButton = document.getElementById("accept");
const referClaimButton = document.getElementById("refer");
const claimStatusText = document.getElementById("claimStatusText");
function getNodeInfoByType(node) {
switch (node.d.type) {
case "accident":
return node.d.date;
case "person":
return `${node.d.lastName}, ${node.d.firstName}`;
case "doctor":
return `Doctor\n${node.d.lastName}, ${node.d.firstName}`;
case "address":
return node.d.address.split(", ").join("\n");
case "vehicle":
return node.d.registration;
default:
return null;
}
}
function getVisibleClaims() {
const visibleClaims = [];
chart.each({ type: "node" }, (node) => {
if (!node.hi && node.d.type === "accident") {
visibleClaims.push(node.id);
}
});
return visibleClaims;
}
function getIdsConnectedToClaim(claimId) {
const distances = chart.graph().distances(claimId, { all: true });
return Object.keys(distances);
}
function matchData(searchTerms, nodeSearchDict) {
const matchedIdsArrays = [];
searchTerms.forEach((searchItem) => {
const matches = nodeSearchDict[searchItem.type][searchItem.searchString];
if (matches) {
const matchingIdsGroup = [searchItem.id, ...matches];
matchedIdsArrays.push(matchingIdsGroup);
}
});
return matchedIdsArrays;
}
async function findMatches() {
findMatchesButton.disabled = true;
// Make dictionary of the hidden node indexed by type and searchString
// and another dictionary of items in claim tree which we will use to search
const nodeSearchDict = {};
const searchTerms = [];
chart.each({ type: "node" }, (node) => {
const searchString = getNodeInfoByType(node);
if (node.hi) {
nodeSearchDict[node.d.type] = nodeSearchDict[node.d.type] || {};
nodeSearchDict[node.d.type][searchString] = nodeSearchDict[node.d.type][searchString] || [];
nodeSearchDict[node.d.type][searchString].push(node.id);
} else {
searchTerms.push({
type: node.d.type,
searchString,
id: node.id,
});
}
});
// Makes an array of grouped ids
matchedGroupsOfIds = matchData(searchTerms, nodeSearchDict);
// for each similar node gets rest of the claim tree
const showIdsList = [];
matchedGroupsOfIds.forEach((matchedIdsGroup) => {
matchedIdsGroup.forEach((id) => {
showIdsList.push(...getIdsConnectedToClaim(id));
});
});
await chart.show(showIdsList, true);
const visibleClaims = getVisibleClaims();
await chart.layout("sequential", { top: visibleClaims, ...layoutOptions });
combineButton.disabled = false;
}
async function uncombineMatches(animate) {
if (comboIds.length) {
await chart.combo().uncombine(comboIds, { animate, select: false });
}
comboIds = [];
}
async function closeClaim() {
await uncombineMatches(false);
matchedGroupsOfIds = [];
await chart.filter(() => false, { type: "node", animate: false });
}
function setClaimStatus(status) {
claimStatusText.innerText = status;
if (status === " Accepted") {
claimStatusText.style.color = "green";
} else if (status === " Referred") {
claimStatusText.style.color = "red";
} else {
claimStatusText.style.color = "gray";
}
}
async function showClaim(claimId) {
const idsToShow = getIdsConnectedToClaim(claimId);
await chart.show(idsToShow, true);
}
function setLinkProperties() {
const linkProperties = [];
chart.each({ type: "link" }, (link) => {
if (!link.hi) {
linkProperties.push({
id: link.id,
c: defaultStyling.claimColour,
w: 3,
border: { ...defaultStyling.linkLabel.border, colour: defaultStyling.claimColour },
});
}
});
chart.setProperties(linkProperties, false);
}
async function resetChart() {
claimList.disabled = true;
findMatchesButton.disabled = true;
combineButton.disabled = true;
uncombineButton.disabled = true;
await closeClaim();
setClaimStatus(" Undecided");
const claimId = claimList.value;
await showClaim(claimId);
setLinkProperties();
await chart.layout("sequential", { top: [claimId], ...layoutOptions });
findMatchesButton.disabled = false;
claimList.disabled = false;
}
function showContextMenu(x, y) {
contextMenu.style.display = "block";
const { width, height } = chart.viewOptions();
const { width: w, height: h } = contextMenu.getBoundingClientRect();
const top = y + h < height ? y : y - h;
const left = x + w < width ? x : x - w;
contextMenu.style.top = `${top}px`;
contextMenu.style.left = `${left}px`;
}
function hideContextMenu() {
contextMenu.style.display = "none";
}
function isMenuGlyph({ type }) {
return type === "glyph";
}
function menuGlyphContextMenu({ x, y, preventDefault, subItem }) {
hideContextMenu();
// only opens context menu if glyph clicked
if (isMenuGlyph(subItem)) {
preventDefault();
showContextMenu(x, y);
}
}
function menuGlyphHoverHandler({ subItem }) {
const id = claimList[claimList.selectedIndex].value;
if (isMenuGlyph(subItem) && !menuGlyphIsHighlighted) {
chart.setProperties({
id,
g: [highlightedMenuGlyph],
});
menuGlyphIsHighlighted = true;
} else if (menuGlyphIsHighlighted) {
chart.setProperties({
id,
g: [menuGlyph],
});
menuGlyphIsHighlighted = false;
}
}
async function combineMatches() {
const matchsComboDefinitions = matchedGroupsOfIds.map((idGroup) => {
const fixedNode = chart.getItem(idGroup[0]);
return {
ids: idGroup,
label: fixedNode.t,
style: {
c: fixedNode.c,
b: fixedNode.b,
bw: 7,
fi: fixedNode.fi,
d: fixedNode.d,
},
position: "first",
};
});
comboIds = await chart.combo().combine(matchsComboDefinitions, { select: false });
}
async function combineButtonHandler() {
combineButton.disabled = true;
await combineMatches();
uncombineButton.disabled = false;
}
async function uncombineButtonHandler() {
uncombineButton.disabled = true;
await uncombineMatches(true);
combineButton.disabled = false;
}
function buildEventHandlers() {
// open the contextMenu when glyph is clicked
chart.on("click", menuGlyphContextMenu);
// when user drags a node, pans, zooms hide the contextMenu
chart.on("drag-start", hideContextMenu);
chart.on("view-change", hideContextMenu);
// changes the menu glyph when hovering over to make clear its interactable
chart.on("hover", menuGlyphHoverHandler);
// hide menu when clicked
contextMenu.addEventListener("click", hideContextMenu);
findMatchesButton.addEventListener("click", findMatches);
combineButton.addEventListener("click", combineButtonHandler);
uncombineButton.addEventListener("click", uncombineButtonHandler);
acceptClaimButton.addEventListener("click", () => setClaimStatus(" Accepted"));
referClaimButton.addEventListener("click", () => setClaimStatus(" Referred"));
claimList.addEventListener("change", resetChart);
}
function addStyles() {
data.items.forEach((dataItem) => {
if (dataItem.type === "node") {
dataItem.hi = true;
dataItem.c = defaultStyling.nodeColour;
dataItem.b = defaultStyling.iconColours[dataItem.d.type];
dataItem.fi = {
t: defaultStyling.typeIcons[dataItem.d.type],
c: defaultStyling.iconColours[dataItem.d.type],
};
dataItem.t = getNodeInfoByType(dataItem);
// Sets styles for claims 1 & 2
if (dataItem.id === "0108" || dataItem.id === "0109") {
dataItem.b = defaultStyling.claimColour;
dataItem.fi = {
c: defaultStyling.claimColour,
t: defaultStyling.typeIcons.accident,
};
// this adds the menu glyph on the main claim
dataItem.g = [menuGlyph];
}
} else {
dataItem.padding = defaultStyling.linkLabel.padding;
dataItem.fbc = "white";
dataItem.border = defaultStyling.linkLabel.border;
}
});
}
async function startKeyLines() {
// Font Icons and images can often be poorly aligned,
// set offsets to the icons to ensure they are centred correctly
const imageAlignment = {};
const imageAlignmentDefinitions = {
"fas fa-user": { dy: -10, e: 1.05 },
};
// List of icons to realign
const icons = Object.keys(imageAlignmentDefinitions);
icons.forEach((icon) => {
imageAlignment[icon] = imageAlignmentDefinitions[icon];
});
const options = {
// use this property to control the amount of alpha blending to apply to background items
backgroundAlpha: 0.1,
logo: "/images/Logo.png",
iconFontFamily: "Font Awesome 5 Free",
handMode: true,
imageAlignment,
hover: 0,
};
chart = await KeyLines.create({ container: "klchart", options });
buildEventHandlers();
addStyles();
chart.load(data);
resetChart();
}
function loadFonts() {
document.fonts.load('24px "Font Awesome 5 Free"').then(startKeyLines);
}
window.addEventListener("DOMContentLoaded", loadFonts); const data = {
type: "LinkChart",
items: [
{ type: "node", id: "0006", d: { type: "doctor", firstName: "Lowell", lastName: "Walters" } },
{ type: "node", id: "0001", d: { type: "person", firstName: "Walter", lastName: "Stewart" } },
{ type: "node", id: "0002", d: { type: "person", firstName: "Krystal", lastName: "Lucas" } },
{ type: "node", id: "0003", d: { type: "person", firstName: "Emily", lastName: "Douglas" } },
{ type: "node", id: "0004", d: { type: "person", firstName: "Kristina", lastName: "Porter" } },
{ type: "node", id: "0005", d: { type: "person", firstName: "Stephen", lastName: "Porter" } },
{ type: "node", id: "0007", d: { type: "person", firstName: "Julia", lastName: "Rodriquez" } },
{ type: "node", id: "0008", d: { type: "person", firstName: "Everett", lastName: "Page" } },
{ type: "node", id: "0009", d: { type: "person", firstName: "Dianna", lastName: "Green" } },
{ type: "node", id: "0010", d: { type: "person", firstName: "Alison", lastName: "Payne" } },
{ type: "node", id: "0011", d: { type: "person", firstName: "Lonnie", lastName: "Kim" } },
{ type: "node", id: "0012", d: { type: "person", firstName: "Mary", lastName: "Wagner" } },
{ type: "node", id: "0226", d: { type: "vehicle", registration: "OIG 5533" } },
{ type: "node", id: "0227", d: { type: "vehicle", registration: "NLY 6" } },
{ type: "node", id: "0228", d: { type: "vehicle", registration: "PG04 DAM" } },
{ type: "node", id: "0229", d: { type: "vehicle", registration: "DA53 RMX" } },
{ type: "node", id: "0230", d: { type: "vehicle", registration: "LE54 HYU" } },
{ type: "node", id: "0231", d: { type: "vehicle", registration: "GG02 ABC" } },
{ type: "node", id: "0232", d: { type: "vehicle", registration: "MJ04 ARM" } },
{ type: "node", id: "0233", d: { type: "vehicle", registration: "R006 CAT" } },
{ type: "node", id: "0234", d: { type: "vehicle", registration: "JR58 XXX" } },
{ type: "node", id: "0236", d: { type: "vehicle", registration: "DA53 RMX" } },
{
type: "node",
id: "0335",
d: { type: "address", address: "3 King's Head Yard, London SE1 1NA" },
},
{ type: "node", id: "0336", d: { type: "address", address: "Valois House, London SE1 3EG" } },
{ type: "node", id: "0337", d: { type: "address", address: "7 Colnbrook St, London SE1 6EZ" } },
{ type: "node", id: "0338", d: { type: "address", address: "160 Tooley St, London SE1 2JP" } },
{
type: "node",
id: "0339",
d: { type: "address", address: "10C Trinity Church Square, London SE1 4HU" },
},
{ type: "node", id: "0340", d: { type: "address", address: "Valois House, London SE1 3EG" } },
{
type: "node",
id: "0342",
d: { type: "address", address: "3 King's Head Yard, London SE1 1NA" },
},
{
type: "node",
id: "0343",
d: { type: "address", address: "75-79 York Rd, Lambeth, London SE1 7NJ" },
},
{
type: "node",
id: "0344",
d: { type: "address", address: "2 King James Ct, London SE1 0DH" },
},
{ type: "node", id: "0346", d: { type: "address", address: "5 Mandela Way, London SE1 5GG" } },
{ type: "node", id: "0356", d: { type: "address", address: "7 Colnbrook St, London SE1 6EZ" } },
{ type: "node", id: "0100", d: { type: "accident", date: "2019-03-16" } },
{ type: "node", id: "0102", d: { type: "accident", date: "2016-04-01" } },
{ type: "node", id: "0105", d: { type: "accident", date: "2019-06-07" } },
{ type: "node", id: "0108", d: { type: "accident", date: "2019-09-27" } },
{ type: "node", id: "0109", d: { type: "accident", date: "2019-11-24" } },
{ type: "link", id: "1000", id1: "0004", id2: "0226", fs: 12, t: "DROVE", a2: true },
{ type: "link", id: "1001", id1: "0002", id2: "0228", fs: 12, t: "DROVE", a2: true },
{ type: "link", id: "1002", id1: "0003", id2: "0227", fs: 12, t: "DROVE", a2: true },
{ type: "link", id: "1003", id1: "0001", id2: "0229", fs: 12, t: "DROVE", a2: true },
{ type: "link", id: "1004", id1: "0005", id2: "0230", fs: 12, t: "DROVE", a2: true },
{ type: "link", id: "1005", id1: "0007", id2: "0236", fs: 12, t: "DROVE", a2: true },
{ type: "link", id: "1105", id1: "0008", id2: "0108", fs: 12, t: "WITNESSED", a2: true },
{ type: "link", id: "1106", id1: "0002", id2: "0105", fs: 12, t: "WITNESSED", a2: true },
{ type: "link", id: "1207", id1: "0006", id2: "0001", fs: 12, t: "TREATED", a2: true },
{ type: "link", id: "1208", id1: "0006", id2: "0004", fs: 12, t: "TREATED", a2: true },
{ type: "link", id: "1309", id1: "0226", id2: "0105", fs: 12, t: "INVOLVED IN", a2: true },
{ type: "link", id: "1310", id1: "0227", id2: "0105", fs: 12, t: "INVOLVED IN", a2: true },
{ type: "link", id: "1311", id1: "0228", id2: "0100", fs: 12, t: "INVOLVED IN", a2: true },
{ type: "link", id: "1312", id1: "0229", id2: "0100", fs: 12, t: "INVOLVED IN", a2: true },
{ type: "link", id: "1313", id1: "0236", id2: "0108", fs: 12, t: "INVOLVED IN", a2: true },
{ type: "link", id: "1314", id1: "0230", id2: "0108", fs: 12, t: "INVOLVED IN", a2: true },
{ type: "link", id: "1401", id1: "0001", id2: "0342", fs: 12, t: "LIVES AT", a2: true },
{ type: "link", id: "1402", id1: "0002", id2: "0343", fs: 12, t: "LIVES AT", a2: true },
{ type: "link", id: "1403", id1: "0003", id2: "0344", fs: 12, t: "LIVES AT", a2: true },
{ type: "link", id: "1404", id1: "0004", id2: "0337", fs: 12, t: "LIVES AT", a2: true },
{ type: "link", id: "1405", id1: "0005", id2: "0356", fs: 12, t: "LIVES AT", a2: true },
{ type: "link", id: "1408", id1: "0007", id2: "0346", fs: 12, t: "LIVES AT", a2: true },
{ type: "link", id: "1409", id1: "0008", id2: "0335", fs: 12, t: "LIVES AT", a2: true },
{ type: "link", id: "1409", id1: "0008", id2: "0335", fs: 12, t: "LIVES AT", a2: true },
{ type: "link", id: "1410", id1: "0009", id2: "0231", fs: 12, t: "DROVE", a2: true },
{ type: "link", id: "1411", id1: "0010", id2: "0232", fs: 12, t: "DROVE", a2: true },
{ type: "link", id: "1412", id1: "0011", id2: "0233", fs: 12, t: "DROVE", a2: true },
{ type: "link", id: "1413", id1: "0012", id2: "0234", fs: 12, t: "DROVE", a2: true },
{ type: "link", id: "1414", id1: "0231", id2: "0109", fs: 12, t: "INVOLVED IN", a2: true },
{ type: "link", id: "1415", id1: "0232", id2: "0109", fs: 12, t: "INVOLVED IN", a2: true },
{ type: "link", id: "1416", id1: "0233", id2: "0102", fs: 12, t: "INVOLVED IN", a2: true },
{ type: "link", id: "1417", id1: "0234", id2: "0102", fs: 12, t: "INVOLVED IN", a2: true },
{ type: "link", id: "1418", id1: "0009", id2: "0339", fs: 12, t: "LIVES AT", a2: true },
{ type: "link", id: "1419", id1: "0010", id2: "0336", fs: 12, t: "LIVES AT", a2: true },
{ type: "link", id: "1420", id1: "0011", id2: "0340", fs: 12, t: "LIVES AT", a2: true },
{ type: "link", id: "1421", id1: "0012", id2: "0338", fs: 12, t: "LIVES AT", a2: true },
],
};
export default data; <!doctype html>
<html lang="en" style="background-color: #2d383f">
<head>
<meta charset="utf-8" />
<title>Insurance Claim Workflows</title>
<link rel="stylesheet" type="text/css" href="@ci/theme/kl/css/keylines.css" />
<link rel="stylesheet" type="text/css" href="@ci/theme/kl/css/minimalsdk.css" />
<link rel="stylesheet" type="text/css" href="@ci/theme/kl/css/sdk-layout.css" />
<link rel="stylesheet" type="text/css" href="@ci/theme/kl/css/demo.css" />
<link
rel="stylesheet"
type="text/css"
href="@fortawesome/[email protected]/css/fontawesome.css"
/>
<link
rel="stylesheet"
type="text/css"
href="@fortawesome/[email protected]/css/solid.css"
/>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div id="klchart" class="klchart"></div>
<script type="module" src="./code.js"></script>
</body>
</html> .modal {
width: 200px;
position: absolute;
margin-left: -100px;
font-size: 16px;
border-radius: 0;
box-shadow: none;
}
.modal-footer {
background-color: white;
text-align: center;
border-top: none;
padding-top: 0;
}