Search

Credit Card Fraud

Time

Investigate the source of credit card fraud.

Credit Card Fraud
View live example →

This demo uses KeyLines to find the origin of a card skimming fraud, which occurs when a skimmer steals a customer's card data during a transaction for later unauthorised use.

The chart shows:

  • Nodes representing people and merchants.
  • Links representing transactions. Red links are disputed, green are undisputed.

Between the chart and the time bar, there is a number of linked features to help you understand the data:

  • Zooming into the time bar's histogram focusses on transactions that happened during the zoomed time period.
  • Selecting a person or a merchant foregrounds all their transactions and shows the values as link labels.
  • Hovering the histogram shows tooltips with details and pings the relevant nodes in the chart.
  • You can use the time bar controls to see how the transactions happened over time.

The featured data is based on Credit Card Fraud Detection scenario created by Neo4j GraphGist.

Key functions used:

import KeyLines from "keylines";
import {
  createChartItems,
  blue,
  lightRed,
  red,
  darkRed,
  lightGreen,
  green,
  darkGreen,
  white,
  black,
  setItemHighlight,
  getSummaryLinkGlyph,
} from "./data.js";

let components;
let chart;
let timebar;

let aggregateCounter = 1;
let items = createChartItems();

const segmentNames = ["Disputed", "Undisputed"];
const segmentBgColors = [darkRed, darkGreen, red, green];
const segmentTextColors = [white, white, black, black];

const tooltipElements = {
  tooltipEl: document.getElementById("tooltip"),
  transactionsEl: document.getElementById("tooltip-transactions"),
  segmentNameEl: document.getElementById("tooltip-segment-name"),
  segmentValueEl: document.getElementById("tooltip-segment-value"),
  valueEl: document.getElementById("tooltip-value"),
  arrowEl: document.querySelector(".tooltip-arrow"),
  innerEl: document.querySelector(".tooltip-inner"),
};

function runLayout() {
  return chart.layout("organic", {
    time: 500,
    straighten: false,
    easing: "linear",
    mode: "adaptive",
    tightness: 4,
  });
}

/**
 * Animates a halo around the nodes within the selected time bar range.
 */
function pingSelection({ type, index, rangeStart, rangeEnd }) {
  if (type !== "bar" && type !== "selection") {
    return;
  }

  const timebarSelection = timebar.selection();
  const selection = chart.selection();
  let colour = blue;
  const ids = timebar.getIds(rangeStart, rangeEnd);
  let hoveredIds = [];

  if (type === "selection") {
    colour = index === 0 ? red : green;
    hoveredIds = ids.filter((id) => timebarSelection[index].includes(id));
  } else if (type === "bar") {
    hoveredIds = ids;
  }
  // Don't ping nodes that are already selected
  if (hoveredIds.length) {
    const neighbours = chart.graph().neighbours(hoveredIds);
    chart.ping(
      neighbours.nodes.filter((node) => !selection.includes(node)),
      { c: colour, time: 1000, w: 30, r: 40, repeat: 1 }
    );
  }
}

function hideTooltip() {
  tooltipElements.tooltipEl.classList.add("fadeout");
}

function showTooltip(hoverEvent) {
  // tooltipX/Y is the recommended position to place a tooltip
  const { type, value, groupIndex, groupValues, tooltipX, tooltipY, rangeStart, rangeEnd } =
    hoverEvent;
  const toShow = type === "bar" || type === "selection";
  if (toShow) {
    const { arrowEl, innerEl, tooltipEl, transactionsEl, valueEl, segmentNameEl, segmentValueEl } =
      tooltipElements;
    // Change the tooltip content
    const transactionCount = timebar.getIds(rangeStart, rangeEnd).length;
    transactionsEl.innerHTML = `${transactionCount}`;
    valueEl.innerHTML = `$${formatNumber(value)}`;
    segmentNameEl.innerHTML = segmentNames[groupIndex % 2];
    segmentValueEl.innerHTML = `$${formatNumber(groupValues[groupIndex])}`;

    // Style both the tooltip body and the arrow
    const backgroundColor = segmentBgColors[groupIndex];
    const textColor = segmentTextColors[groupIndex];
    arrowEl.style.borderTopColor = backgroundColor;
    innerEl.style.backgroundColor = backgroundColor;
    innerEl.style.color = textColor;
    // The top needs to be adjusted to accommodate the height of the tooltip
    const tooltipTop = tooltipY - tooltipEl.offsetHeight;
    // Shift left by half width to centre it
    const tooltipLeft = tooltipX - tooltipEl.offsetWidth / 2;
    // Set the position of the tooltip
    tooltipEl.style.left = `${tooltipLeft}px`;
    tooltipEl.style.top = `${tooltipTop}px`;
    // Show the tooltip
    tooltipEl.classList.remove("fadeout");
  } else {
    hideTooltip();
  }
}

function onSelectionChange() {
  const selectedIds = chart.selection().filter((id) => chart.getItem(id).type === "node");

  if (selectedIds.length === 0) {
    // clear the selection
    chart.foreground(() => true);
    timebar.selection([]);
  } else {
    const nodesToHighlight = getNodesToHighlight(selectedIds);
    chart.foreground((node) => nodesToHighlight.has(node.id));
  }

  updateLinkState(selectedIds);
}

function updateLinkState(selectedIds) {
  const selectedIdsSet = new Set(selectedIds);
  const modifiedLinks = [];
  chart.each({ type: "link" }, (link) => {
    if (link.d.summaryLinkIds) return;

    const selected = selectedIdsSet.has(link.id1) || selectedIdsSet.has(link.id2);
    const foregrounded = selectedIdsSet.size === 0 || selected;
    link.d.status = `${link.d.disputed ? "disputed" : "undisputed"}${foregrounded ? "" : "-bg"}`;
    link.d.aggregateId = selected || link.d.userDisaggregated ? ++aggregateCounter : 0;
    link.d.selected = selected;
    modifiedLinks.push(link);
  });
  timebar.merge(modifiedLinks);
  chart.merge(modifiedLinks);
}

function getNodesToHighlight(selectedIds) {
  const nodesToHighlight = new Set(selectedIds);
  const undisputedStoreVisitorCounts = new Map();
  const selectionKinds = new Set(selectedIds.map((nodeId) => chart.getItem(nodeId).d.label));

  // Where only one kind of item is selected, highlight what it links to
  if (selectionKinds.size === 1) {
    const neighbours = chart.graph().neighbours(selectedIds);
    neighbours.nodes.forEach((nodeId) => nodesToHighlight.add(nodeId));
    neighbours.links.forEach((linkId) => {
      const link = chart.getItem(linkId);
      let ends = undisputedStoreVisitorCounts.get(link.id2);
      if (ends == null) {
        ends = new Set();
        undisputedStoreVisitorCounts.set(link.id2, ends);
      }
      ends.add(link.id1);
    });
    // Where only people are selected, remove stores without common undisputed transactions
    if (selectionKinds.has("person")) {
      undisputedStoreVisitorCounts.forEach((linkedNodeIds, id) => {
        if (linkedNodeIds.size !== selectedIds.length) {
          nodesToHighlight.delete(id);
        }
      });
    }
  }

  return nodesToHighlight;
}

async function filterChartByRange() {
  await chart.filter(timebar.inRange, { type: "link", animate: false });
  await runLayout();
  onSelectionChange();
}

function formatNumber(num) {
  if (num == null) return "";
  const fixed = num.toFixed(2);
  return fixed.endsWith(".00") ? Math.floor(num).toString() : fixed;
}

function setAggregateLinkStyle(summary) {
  const containsDispute = summary.links.some((id) => chart.getItem(id).d.disputed);
  const isSelected = summary.links.some((id) => chart.getItem(id).d.selected);
  const userDisaggregated = summary.links.some((id) => chart.getItem(id).d.userDisaggregated);
  const totalTransactionValue = summary.links.reduce(
    (total, id) => total + chart.getItem(id).d.amount,
    0
  );

  chart.setProperties({
    id: summary.id,
    c: containsDispute ? darkRed : green,
    w: 4,
    g: getSummaryLinkGlyph(summary, containsDispute),
    padding: "3 3 0 3",
    fbc: containsDispute ? darkRed : darkGreen,
    fc: "white",
    t: isSelected ? `$${formatNumber(totalTransactionValue)}` : undefined,
    border: {
      radius: 8,
    },
    d: {
      summaryLinkIds: summary.links,
      userDisaggregated: userDisaggregated,
      disputed: containsDispute,
      status: containsDispute ? "disputed" : "undisputed",
      linkCount: summary.links.length,
    },
  });
}

function getLink(id) {
  const item = chart.getItem(id);
  return item != null && item.type === "link" ? item : null;
}

function disaggregateSummaryLink(summaryLink) {
  const children = chart.getAggregateInfo(summaryLink.id).aggregateChildren;
  for (let i = 0; i < children.length; ++i) {
    const childId = children[i];
    const item = chart.getItem(childId);
    chart.setProperties({
      id: children[i],
      d: {
        ...item.d,
        aggregateId: ++aggregateCounter,
        userDisaggregated: true,
      },
    });
  }
}

function initialiseEventing() {
  let prevHoveredItemId = null;

  chart.on("hover", ({ id, subItem }) => {
    if (prevHoveredItemId !== id && prevHoveredItemId != null) {
      setItemHighlight(chart, prevHoveredItemId, false);
    }
    if (id != null) {
      setItemHighlight(chart, id, true);
      prevHoveredItemId = id;
    }
  });

  chart.on("link-aggregation", setAggregateLinkStyle);

  // Remove present tooltip and filter the chart as the timebar changes
  timebar.on("change", () => {
    hideTooltip();
    filterChartByRange();
  });

  let pingInterval = null;
  // Display tooltip and ping relevant nodes
  timebar.on("hover", (hoverEvent) => {
    showTooltip(hoverEvent);

    // Pings immediately on hovering
    pingSelection(hoverEvent);

    // Clears any existing intervals to avoid duplicate pings
    clearInterval(pingInterval);

    // triggers a ping every 2s while hovering
    pingInterval = setInterval(() => {
      pingSelection(hoverEvent);
    }, 2000);
  });

  // When hover ends stop the pinging and reset interval
  timebar.on("hoverEnd", () => {
    clearInterval(pingInterval);
    pingInterval = null;
  });

  chart.on("click", ({ preventDefault, id, subItem }) => {
    const link = getLink(id);
    if (link) {
      const glyphClicked = subItem.type === "glyph";
      const canBeDisaggregated = link.d.summaryLinkIds?.length > 1 && !link.d.userDisaggregated;
      if (glyphClicked && canBeDisaggregated) {
        disaggregateSummaryLink(link);
      }
      // links should not be selectable
      preventDefault();
    }
  });
  chart.on("selection-change", onSelectionChange);

  // Reset the chart and timebar
  document.getElementById("reset").addEventListener("click", async () => {
    await chart.merge(items);
    await timebar.merge(items);
    chart.selection([]);
    onSelectionChange();
    timebar.zoom("fit", {});
  });

  document.getElementById("recombine").addEventListener("click", async () => {
    const updates = [];
    chart.each({ type: "link" }, (link) => {
      if (link.d.aggregateId != null) {
        updates.push({ id: link.id, d: { ...link.d, userDisaggregated: false } });
      }
    });
    chart.setProperties(updates);
    onSelectionChange();
  });

  // Foreground person nodes with disputed transactions
  document.getElementById("select").addEventListener("click", () => {
    const toSelect = [];
    chart.each({ type: "node" }, (node) => {
      if (node.d.hasDisputes && node.d.label === "person") {
        toSelect.push(node.id);
      }
    });
    chart.selection(toSelect);
    onSelectionChange();
  });
}

async function startKeyLines() {
  const options = {
    hover: 4,
    marqueeLinkSelection: "off",
    handMode: true,
    selectionColour: blue,
    selectedNode: {
      bw: 0,
      ha0: {
        c: blue,
        r: 20,
        w: 20,
      },
    },
    // Use this property to control the amount of alpha blending to apply to background items
    backgroundAlpha: 0.25,
    imageAlignment: {
      // Size and centre icons
      "fas fa-shopping-cart": { e: 0.7, dx: -7 },
      "fas fa-user": { dy: -7, e: 0.77 },
    },
    iconFontFamily: "Font Awesome 5 Free",
    logo: "/images/Logo.png",
    aggregateLinks: { aggregateBy: "aggregateId" },
    drag: {
      links: false,
    },
  };

  const tbOptions = {
    showExtend: true,
    playSpeed: 30,
    minScale: { units: "day", value: 1 },
    groups: {
      groupBy: "status",
      categories: ["disputed", "undisputed", "disputed-bg", "undisputed-bg"],
      colours: [darkRed, green, lightRed, lightGreen],
    },
  };

  components = await KeyLines.create([
    { container: "klchart", type: "chart", options },
    { container: "kltimebar", type: "timebar", options: tbOptions },
  ]);

  chart = components[0];
  timebar = components[1];

  initialiseEventing();
  await chart.load(items);
  await timebar.load(items);
  onSelectionChange();
  await timebar.zoom("fit", {});
  await runLayout();
}

async function loadKeyLines() {
  await document.fonts.load('24px "Font Awesome 5 Free"');
  await startKeyLines();
}

window.addEventListener("DOMContentLoaded", loadKeyLines);
export const darkRed = "rgb(180,21,23)";
export const red = "rgb(255,189,190)";
export const lightRed = "rgb(248,232,232)";
export const darkGreen = "rgb(36,130,38)";
export const green = "rgb(154,219,154)";
export const lightGreen = "rgb(235,248,235)";
export const blue = "rgb(50,115,213)";
export const grey = "rgb(99,115,141)";
export const lightGrey = "rgb(200,209,222)";
export const white = "rgb(255,255,255)";
export const black = "rgb(0,0,0)";

const nodeTextStyle = {
  position: "s",
  borderRadius: 90,
  padding: {
    top: 3,
    left: 6,
    right: 6,
  },
};

export function createChartItems() {
  const items = [];

  for (const store of data.stores) {
    items.push({
      id: store.id,
      type: "node",
      t: [
        {
          ...nodeTextStyle,
          t: store.name,
        },
      ],
      c: lightGrey,
      fi: {
        c: grey,
        t: "fas fa-shopping-cart",
      },
      d: {
        label: "merchant",
        hasDisputes: store.hasDisputes,
      },
    });
  }

  for (const customer of data.customers) {
    items.push({
      id: customer.id,
      type: "node",
      c: customer.hasDisputes ? red : green,
      t: [
        {
          ...nodeTextStyle,
          t: customer.name,
        },
      ],
      fi: {
        c: customer.hasDisputes ? darkRed : darkGreen,
        t: "fas fa-user",
      },
      d: {
        label: "person",
        hasDisputes: customer.hasDisputes,
      },
    });
  }

  for (const transaction of data.transactions) {
    items.push({
      id: transaction.id,
      type: "link",
      id1: transaction.id1,
      id2: transaction.id2,
      a2: true,
      c: transaction.disputed ? red : green,
      w: 2,
      v: transaction.amount,
      d: {
        amount: transaction.amount,
        time: transaction.time,
        disputed: transaction.disputed,
        status: transaction.disputed ? "disputed" : "undisputed",
        aggregateId: 0,
      },
      dt: transaction.time,
    });
  }

  return { type: "LinkChart", items: items };
}

export function setItemHighlight(chart, id, highlighted) {
  const item = chart.getItem(id);
  if (item != null) {
    if (item.type === "link" && item.g != null) {
      item.g[0].c = highlighted ? blue : item.d.disputed ? darkRed : darkGreen;
    } else if (item.d.label === "person") {
      item.c = highlighted ? blue : item.d.hasDisputes ? red : green;
      item.fi.c = highlighted ? white : item.d.hasDisputes ? darkRed : darkGreen;
    } else if (item.d.label === "merchant") {
      item.c = highlighted ? blue : lightGrey;
      item.fi.c = highlighted ? white : grey;
    }
    chart.setProperties(item);
  }
}

export function getSummaryLinkGlyph(summary, containsDispute) {
  return summary.links.length === 1
    ? undefined
    : [
        {
          p: "ne",
          c: containsDispute ? darkRed : darkGreen,
          fi: {
            c: white,
            t: "fa-s fa-expand-alt",
          },
          border: { colour: containsDispute ? red : green },
        },
      ];
}

const data = {
  stores: [
    {
      id: "10",
      name: "Amazon",
    },
    {
      id: "11",
      name: "Abercrombie",
    },
    {
      id: "12",
      name: "Walmart",
    },
    {
      id: "13",
      name: "McDonald's",
    },
    {
      id: "14",
      name: "American Apparel",
    },
    {
      id: "15",
      name: "Just Brew It",
    },
    {
      id: "16",
      name: "Justice",
    },
    {
      id: "17",
      name: "Sears",
    },
    {
      id: "18",
      name: "Soccer for the City",
    },
    {
      id: "19",
      name: "Sprint",
    },
    {
      id: "20",
      name: "Starbucks",
    },
    {
      id: "21",
      name: "Subway",
    },
    {
      id: "22",
      name: "Apple Store",
      hasDisputes: true,
    },
    {
      id: "23",
      name: "Urban Outfitters",
      hasDisputes: true,
    },
    {
      id: "24",
      name: "RadioShack",
      hasDisputes: true,
    },
    {
      id: "25",
      name: "Macy's",
      hasDisputes: true,
    },
  ],
  customers: [
    {
      id: "0",
      name: "Paul",
      hasDisputes: true,
    },
    {
      id: "1",
      name: "Jean",
    },
    {
      id: "2",
      name: "Dan",
    },
    {
      id: "3",
      name: "Marc",
      hasDisputes: true,
    },
    {
      id: "4",
      name: "John",
    },
    {
      id: "5",
      name: "Zoey",
    },
    {
      id: "6",
      name: "Ava",
    },
    {
      id: "7",
      name: "Olivia",
      hasDisputes: true,
    },
    {
      id: "8",
      name: "Mia",
    },
    {
      id: "9",
      name: "Madison",
      hasDisputes: true,
    },
  ],
  transactions: [
    {
      id: "l1",
      id1: "0",
      id2: "25",
      amount: 849,
      time: 1419033600000,
      disputed: true,
    },
    {
      id: "l2",
      id1: "0",
      id2: "24",
      amount: 415,
      time: 1396310400000,
      disputed: true,
    },
    {
      id: "l3",
      id1: "0",
      id2: "23",
      amount: 732,
      time: 1399680000000,
      disputed: true,
    },
    {
      id: "l4",
      id1: "0",
      id2: "22",
      amount: 351.66,
      time: 1405641600000,
      disputed: true,
    },
    {
      id: "l5",
      id1: "0",
      id2: "12",
      amount: 654,
      time: 1395273600000,
    },
    {
      id: "l6",
      id1: "0",
      id2: "17",
      amount: 475,
      time: 1395964800000,
    },
    {
      id: "l7",
      id1: "0",
      id2: "20",
      amount: 2.39,
      time: 1400112000000,
    },
    {
      id: "l8",
      id1: "0",
      id2: "15",
      amount: 9.86,
      time: 1397692800000,
    },
    {
      id: "l9",
      id1: "1",
      id2: "21",
      amount: 2.03,
      time: 1395878400000,
    },
    {
      id: "l10",
      id1: "1",
      id2: "10",
      amount: 802,
      time: 1394496000000,
    },
    {
      id: "l11",
      id1: "1",
      id2: "12",
      amount: 848,
      time: 1401321600000,
    },
    {
      id: "l12",
      id1: "1",
      id2: "11",
      amount: 50.2,
      time: 1397001600000,
    },
    {
      id: "l13",
      id1: "1",
      id2: "18",
      amount: 196,
      time: 1406160000000,
    },
    {
      id: "l14",
      id1: "2",
      id2: "10",
      amount: 141,
      time: 1415923200000,
    },
    {
      id: "l15",
      id1: "2",
      id2: "18",
      amount: 62,
      time: 1410912000000,
    },
    {
      id: "l16",
      id1: "2",
      id2: "13",
      amount: 6.05,
      time: 1390780800000,
    },
    {
      id: "l17",
      id1: "2",
      id2: "13",
      amount: 3.5,
      time: 1390435200000,
    },
    {
      id: "l18",
      id1: "3",
      id2: "25",
      amount: 1003,
      time: 1419033600000,
      disputed: true,
    },
    {
      id: "l19",
      id1: "3",
      id2: "24",
      amount: 1121,
      time: 1396310400000,
      disputed: true,
    },
    {
      id: "l20",
      id1: "3",
      id2: "23",
      amount: 424,
      time: 1399680000000,
      disputed: true,
    },
    {
      id: "l21",
      id1: "3",
      id2: "22",
      amount: 814,
      time: 1405641600000,
      disputed: true,
    },
    {
      id: "l22",
      id1: "3",
      id2: "18",
      amount: 11,
      time: 1409788800000,
    },
    {
      id: "l23",
      id1: "3",
      id2: "17",
      amount: 430,
      time: 1407628800000,
    },
    {
      id: "l24",
      id1: "3",
      id2: "12",
      amount: 964,
      time: 1395446400000,
    },
    {
      id: "l25",
      id1: "3",
      id2: "14",
      amount: 33.6,
      time: 1396483200000,
    },
    {
      id: "l26",
      id1: "3",
      id2: "10",
      amount: 134,
      time: 1397433600000,
    },
    {
      id: "l27",
      id1: "4",
      id2: "15",
      amount: 9.21,
      time: 1394582400000,
    },
    {
      id: "l28",
      id1: "4",
      id2: "14",
      amount: 76.8,
      time: 1417132800000,
    },
    {
      id: "l29",
      id1: "4",
      id2: "16",
      amount: 468,
      time: 1406592000000,
    },
    {
      id: "l30",
      id1: "4",
      id2: "19",
      amount: 457,
      time: 1413331200000,
    },
    {
      id: "l31",
      id1: "4",
      id2: "18",
      amount: 545,
      time: 1412553600000,
    },
    {
      id: "l32",
      id1: "5",
      id2: "21",
      amount: 3.53,
      time: 1414195200000,
    },
    {
      id: "l33",
      id1: "5",
      id2: "10",
      amount: 721,
      time: 1405555200000,
    },
    {
      id: "l34",
      id1: "5",
      id2: "15",
      amount: 4.14,
      time: 1390176000000,
    },
    {
      id: "l35",
      id1: "5",
      id2: "11",
      amount: 51.0,
      time: 1417046400000,
    },
    {
      id: "l36",
      id1: "5",
      id2: "13",
      amount: 7.4,
      time: 1418601600000,
    },
    {
      id: "l37",
      id1: "6",
      id2: "15",
      amount: 6.27,
      time: 1400544000000,
    },
    {
      id: "l38",
      id1: "6",
      id2: "14",
      amount: 72.3,
      time: 1389139200000,
    },
    {
      id: "l39",
      id1: "6",
      id2: "14",
      amount: 53.3,
      time: 1407283200000,
    },
    {
      id: "l40",
      id1: "6",
      id2: "12",
      amount: 87,
      time: 1392768000000,
    },
    {
      id: "l41",
      id1: "6",
      id2: "17",
      amount: 681,
      time: 1419724800000,
    },
    {
      id: "l42",
      id1: "7",
      id2: "25",
      amount: 790,
      time: 1419033600000,
      disputed: true,
    },
    {
      id: "l43",
      id1: "7",
      id2: "24",
      amount: 884,
      time: 1406851200000,
      disputed: true,
    },
    {
      id: "l44",
      id1: "7",
      id2: "23",
      amount: 1152,
      time: 1407628800000,
      disputed: true,
    },
    {
      id: "l45",
      id1: "7",
      id2: "22",
      amount: 1149,
      time: 1405641600000,
      disputed: true,
    },
    {
      id: "l46",
      id1: "7",
      id2: "15",
      amount: 7.42,
      time: 1407801600000,
    },
    {
      id: "l47",
      id1: "7",
      id2: "18",
      amount: 924,
      time: 1412380800000,
    },
    {
      id: "l48",
      id1: "7",
      id2: "12",
      amount: 231,
      time: 1405123200000,
    },
    {
      id: "l49",
      id1: "7",
      id2: "18",
      amount: 74,
      time: 1409788800000,
    },
    {
      id: "l50",
      id1: "8",
      id2: "18",
      amount: 164,
      time: 1419552000000,
    },
    {
      id: "l51",
      id1: "8",
      id2: "10",
      amount: 240,
      time: 1404864000000,
    },
    {
      id: "l52",
      id1: "8",
      id2: "17",
      amount: 830,
      time: 1394668800000,
    },
    {
      id: "l53",
      id1: "8",
      id2: "13",
      amount: 4.67,
      time: 1419292800000,
    },
    {
      id: "l54",
      id1: "8",
      id2: "20",
      amount: 6.6,
      time: 1397606400000,
    },
    {
      id: "l55",
      id1: "8",
      id2: "18",
      amount: 276,
      time: 1419379200000,
    },
    {
      id: "l56",
      id1: "9",
      id2: "25",
      amount: 816,
      time: 1419033600000,
      disputed: true,
    },
    {
      id: "l57",
      id1: "9",
      id2: "24",
      amount: 368,
      time: 1404172800000,
      disputed: true,
    },
    {
      id: "l58",
      id1: "9",
      id2: "23",
      amount: 374,
      time: 1404950400000,
      disputed: true,
    },
    {
      id: "l59",
      id1: "9",
      id2: "22",
      amount: 925,
      time: 1405641600000,
      disputed: true,
    },
    {
      id: "l60",
      id1: "9",
      id2: "12",
      amount: 91,
      time: 1404000000000,
    },
    {
      id: "l61",
      id1: "9",
      id2: "10",
      amount: 147,
      time: 1407024000000,
    },
    {
      id: "l62",
      id1: "9",
      id2: "21",
      amount: 3.52,
      time: 1418688000000,
    },
    {
      id: "l63",
      id1: "9",
      id2: "11",
      amount: 19,
      time: 1406592000000,
    },
    {
      id: "l64",
      id1: "9",
      id2: "13",
      amount: 4.99,
      time: 1412553600000,
    },
    {
      id: "l65",
      id1: "2",
      id2: "13",
      amount: 5.32,
      time: 1390348800000,
    },
    {
      id: "l66",
      id1: "2",
      id2: "13",
      amount: 7.99,
      time: 1390435200000 - 86400000 * 2,
    },
    {
      id: "l67",
      id1: "9",
      id2: "13",
      amount: 4.99,
      time: 1390262400000,
    },
    {
      id: "l68",
      id1: "9",
      id2: "13",
      amount: 4.99,
      time: 1412726400000,
    },
    {
      id: "l69",
      id1: "9",
      id2: "21",
      amount: 3.52,
      time: 1418774400000,
    },
    {
      id: "l70",
      id1: "9",
      id2: "21",
      amount: 3.52,
      time: 1418860800000,
    },
  ],
};
<!doctype html>
<html lang="en" style="background-color: #2d383f">
  <head>
    <meta charset="utf-8" />
    <title>Credit Card Fraud</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" type="text/css" href="creditcard.css" />
    <link rel="stylesheet" href="./style.css" />
  </head>
  <body>
    <div id="klchart" class="klchart klchart-timebar"></div>
    <div id="kltimebar" class="kltimebar"></div>
    <script type="module" src="./code.js"></script>
  </body>
</html>
#tooltip .tooltip-arrow {
  position: absolute;
  width: 0;
  height: 0;
  border-color: transparent;
  border-style: solid;
}

.tooltip-arrow-down {
  bottom: 0;
  left: 50%;
  margin-left: -5px;
  border-top-color: #000;
  border-width: 5px 5px 0;
}

#tooltip {
  top: 200px;
  position: absolute;
  pointer-events: none;
  display: block;
  float: left;
  color: white;
  font-size: 12px;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  transition: opacity 0.3s ease;
}

#tooltip tr {
  height: 20px;
}

#tooltip.fadeout {
  opacity: 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.