Search

Insurance Claim Workflows

Analysis

Find matches and flag fraudulent insurance claims.

Insurance Claim Workflows
View live example →

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

Terms of use

These terms do not alter or supersede any existing agreements between you (or your employer) and us.

By accessing or using any Content you agree to be bound by these Terms of Use. Please review these terms carefully before using the website.

The contents of this website, including but not limited to any text, code samples, API references, schemas, interactive tools, and other materials (collectively, the 'Content'), are made available for informational and internal evaluation purposes only. All intellectual property rights in the Content are reserved. No licence is granted to use the Content for any commercial purpose, or to copy, distribute, modify, reverse-engineer, or incorporate any part of the Content into any product or service, without our prior written consent.

This Content is provided “as is” and “as available,” without any representations, warranties, or guarantees of any kind, whether express or implied, including but not limited to implied warranties of merchantability, fitness for a particular purpose, non-infringement, or accuracy. To the fullest extent permitted by applicable law, we expressly exclude and disclaim all implied warranties, conditions, and other terms that might otherwise be implied.

We disclaim all liability for any loss or damage, whether direct, indirect, incidental, consequential, or otherwise, arising from any reliance placed on the Content or from your use of it, to the fullest extent permitted by applicable law. By continuing to access or use the Content, you acknowledge and agree to these terms.