Search

Mafia Network

Filtering

Disrupt a resilient mafia network through social network analysis.

Mafia Network
View live example →

Criminal networks are known to be well organised and resilient to disruptions. This demo shows how social network analysis can play an important role in successfully damaging such a network.

Whenever items are expanded or removed, the chart will adjust the node positions adaptively.

The nodes can be removed in two scenarios. You can select one or more individuals to be “arrested” and click on Remove selected. Alternatively, you can simulate a “police raid” and Remove n largest nodes (based on degrees score), which causes even greater disruption to the network.

The nodes removed directly will ping blue, while nodes removed indirectly (by losing connection to any other node) will ping red.

Although the names are fictional, the featured data is based on a study Disrupting Resilient Criminal Networks through Data Analysis: The case of Sicilian Mafia.

Key functions used:

import KeyLines from "keylines";
import { data } from "./data.js";

let visibleChart;
let hiddenChart;
let savedChartItems;
let nodeSelection = [];
let nodeIdsRemovedBySlider = [];
let allNodeIds = [];
const familyIdsCache = {};
const allNodeIdsLookup = {};

// Stacks to maintain the order of items filtered by manual selection and the slider
let manuallyRemovedStack = [];
let sliderStack = [];

// Get elements from the UI
const familyCheckBoxesEls = [
  ...document.querySelectorAll("input[type=checkbox]"),
];
const largestCompSizeEl = document.getElementById("lcc");
const restoreItemsButtonEl = document.getElementById("restore");
const removeItemsButtonEl = document.getElementById("remove");
const sliderEl = document.getElementById("slider");
const resetAllEl = document.getElementById("resetAll");

function setButtonAvailability(element, available) {
  element.classList[available ? "add" : "remove"](["active", "btn-kl"]);
  element.disabled = !available;
}

function setUIAvailability(available) {
  if (manuallyRemovedStack.length) {
    setButtonAvailability(restoreItemsButtonEl, available);
  }
  familyCheckBoxesEls.forEach((radio) => {
    radio.disabled = !available;
  });
  sliderEl.disabled = !available;
  resetAllEl.disabled = !available;
}

// Update order of available nodes for the slider
function setIdsForSliderStack() {
  const sortedAvailableNodeIds = allNodeIds
    // Available nodes are those on the visible chart
    .filter(
      (nodeId) =>
        allNodeIdsLookup[nodeId].sliderAvailability &&
        allNodeIdsLookup[nodeId].filterState,
    )
    .sort(
      (idA, idB) => allNodeIdsLookup[idB].index - allNodeIdsLookup[idA].index,
    );

  // Append items already filtered by the slider to the end of the stack, in order they were removed
  sliderStack = sortedAvailableNodeIds.concat(nodeIdsRemovedBySlider);
}

function setLargestComponentSize() {
  const components = visibleChart.graph().components();
  largestCompSizeEl.innerText = components.length
    ? components.reduce((a, b) => (b.nodes.length < a.nodes.length ? a : b))
        .nodes.length
    : 0;
}

function setNodeState(nodeIds, property, state) {
  nodeIds.forEach((id) => {
    allNodeIdsLookup[id][property] = state;
  });
}

function setNodesManuallySelected(newSelectedNodes) {
  // Clear the previously selected nodes
  if (nodeSelection.length) {
    setNodeState(nodeSelection, "filterState", true);
    nodeSelection = [];
  }
  if (newSelectedNodes.length) {
    // Prepare selected nodes to be removed
    nodeSelection = newSelectedNodes;
    setNodeState(newSelectedNodes, "filterState", false);

    // Highlight neighbours of selected items
    const neighbours = visibleChart.graph().neighbours(newSelectedNodes).nodes;
    visibleChart.foreground(
      (node) =>
        newSelectedNodes.includes(node.id) || neighbours.includes(node.id),
    );

    // Allow selected nodes to be removed
    setButtonAvailability(removeItemsButtonEl, true);
  } else {
    // No nodes selected
    visibleChart.selection([]);
    visibleChart.foreground(() => true);
    setButtonAvailability(removeItemsButtonEl, false);
  }
}

async function pingNodes(allNodeIdsToRemove, primaryNodeIds) {
  const collateralNodeIdsToRemove = allNodeIdsToRemove.filter(
    (id) => !primaryNodeIds.includes(id),
  );
  if (collateralNodeIdsToRemove.length) {
    // Primary removed nodes ping blue
    visibleChart.ping(allNodeIdsToRemove, { time: 1200, c: "#5d81f8" });

    // Collateral nodes ping red
    await visibleChart.ping(collateralNodeIdsToRemove, {
      time: 1200,
      c: "#FF0000",
    });
  } else {
    await visibleChart.ping(allNodeIdsToRemove, { time: 1200, c: "#5d81f8" });
  }
}

async function getItemsFromFilter() {
  function matchFilter(node) {
    return (
      allNodeIdsLookup[node.id].familyCheckbox &&
      allNodeIdsLookup[node.id].filterState
    );
  }

  // Filter the hidden chart and return the items to be removed or expanded into visible chart
  const { shown, hidden } = await hiddenChart.filter(matchFilter, {
    type: "node",
    animate: false,
    hideSingletons: true,
  });
  const nodesShown = shown.nodes;
  const nodesHidden = hidden.nodes;
  const itemsShown = nodesShown.length ? nodesShown.concat(shown.links) : [];
  const itemsHidden = nodesHidden.length
    ? nodesHidden.concat(hidden.links)
    : [];
  return {
    nodesShown,
    nodesHidden,
    itemsShown,
    itemsHidden,
  };
}

async function doFiltering(filteredBy) {
  // Disable the UI while visible chart is updated
  setUIAvailability(false);

  // Get items to be expanded or removed
  const {
    nodesShown: nodeIdsToExpand,
    nodesHidden: nodeIdsToRemove,
    itemsShown: itemIdsToExpand,
    itemsHidden: itemIdsToRemove,
  } = await getItemsFromFilter();

  if (itemIdsToExpand.length) {
    // Allow expanded items to be available for filtering on the slider
    setNodeState(nodeIdsToExpand, "sliderAvailability", true);

    // Retrieve saved item so we can expand with the correct node size
    const itemsToExpand = savedChartItems.filter((item) =>
      itemIdsToExpand.includes(item.id),
    );
    await visibleChart.expand(itemsToExpand, {
      layout: { name: "organic", fit: true },
    });
  } else if (itemIdsToRemove.length) {
    let primaryNodesToHide;

    // Check which filter was used and identify primary nodes for correct ping colour
    if (filteredBy === "selected") {
      manuallyRemovedStack.push(nodeIdsToRemove);
      primaryNodesToHide = nodeSelection;

      // Remove the node selection so that the remove animation is consistent with other filters
      nodeSelection = [];
      setNodesManuallySelected([]);
    } else if (["batanesi", "mistretta", "none"].includes(filteredBy)) {
      primaryNodesToHide = familyIdsCache[filteredBy];
    } else if (filteredBy === "slider") {
      primaryNodesToHide = nodeIdsRemovedBySlider;
    }

    // Make removed nodes unavailable to the slider
    setNodeState(nodeIdsToRemove, "sliderAvailability", false);

    // Ping nodes to be removed
    await pingNodes(nodeIdsToRemove, primaryNodesToHide);

    // Remove items from the visible chart
    await visibleChart.removeItem(nodeIdsToRemove);

    // Layout items
    await visibleChart.layout("organic", { mode: "adaptive" });
  }

  // Update the slider for the nodes remaining on the visible chart
  setIdsForSliderStack();

  // Update the largest component indicator
  setLargestComponentSize();

  // Enable the UI after the visible chart has been updated
  setUIAvailability(true);
}

function initialiseInteractions() {
  const sliderValueEl = document.getElementById("sliderValue");

  // Filter the chart when any checkbox is changed
  familyCheckBoxesEls.forEach((checkbox) => {
    checkbox.addEventListener("click", (event) => {
      let familyIdsToBeFiltered;
      const familyName = event.target.id;
      const familyChecked = event.target.checked;
      setNodesManuallySelected([]);

      // Check if family has been filtered before
      if (familyIdsCache[familyName]) {
        familyIdsToBeFiltered = familyIdsCache[familyName];
      } else {
        // Get the family ids and cache the ids for later use
        familyIdsToBeFiltered = allNodeIds.filter(
          (id) => allNodeIdsLookup[id].family === familyName,
        );
        familyIdsCache[familyName] = familyIdsToBeFiltered;
      }

      setNodeState(familyIdsToBeFiltered, "familyCheckbox", familyChecked);
      doFiltering(familyName);
    });
  });

  visibleChart.on("selection-change", () => {
    const selectedItems = visibleChart.selection();

    // Filter selection to include only nodes
    const newSelectedNodes = selectedItems.filter((id) => !id.match(/-/));
    setNodesManuallySelected(newSelectedNodes);
  });

  // Remove manually selected nodes
  removeItemsButtonEl.addEventListener("click", async () => {
    setButtonAvailability(removeItemsButtonEl, false);
    await doFiltering("selected");
    setButtonAvailability(restoreItemsButtonEl, true);
  });

  // Restore previously removed nodes
  restoreItemsButtonEl.addEventListener("click", () => {
    setNodesManuallySelected([]);

    // Update state of nodes to be restoreed
    setNodeState(manuallyRemovedStack.pop(), "filterState", true);

    // Disable the restore button if stack is empty
    if (manuallyRemovedStack.length === 0) {
      setButtonAvailability(restoreItemsButtonEl, false);
    }
    doFiltering();
  });

  // Update the slider value before the change event if it is dragged
  sliderEl.addEventListener("input", () => {
    sliderValueEl.innerText = +sliderEl.value;
  });

  // Filter the chart once the slider value has been changed
  sliderEl.addEventListener("change", () => {
    setNodesManuallySelected([]);
    const sliderValue = +sliderEl.value;
    const availableNodes = sliderStack.length;
    const nodesToRemove = Math.min(sliderValue, availableNodes);
    nodeIdsRemovedBySlider = sliderStack.slice(
      availableNodes - nodesToRemove,
      availableNodes,
    );

    // Update filter state of nodes on the stack
    sliderStack.forEach((id) => {
      allNodeIdsLookup[id].filterState = !nodeIdsRemovedBySlider.includes(id);
    });
    doFiltering("slider");
  });

  resetAllEl.addEventListener("click", async () => {
    // Clear all filters and reset the UI
    setNodesManuallySelected([]);
    manuallyRemovedStack = [];
    nodeIdsRemovedBySlider = [];
    setButtonAvailability(restoreItemsButtonEl, false);
    sliderEl.value = 0;
    sliderValueEl.innerText = 0;
    familyCheckBoxesEls.forEach((checkbox) => {
      checkbox.checked = true;
    });

    // Update state of all nodes and filter the chart
    setNodeState(allNodeIds, "filterState", true);
    setNodeState(allNodeIds, "familyCheckbox", true);
    await doFiltering();
    visibleChart.layout("organic", { mode: "adaptive" });
  });
}

// Set the lookup for the ranking and availability of each node for the slider
function setNodesIdsLookup(sizedNodes) {
  // All node ids sorted by degree score
  allNodeIds = sizedNodes
    .sort((nodeA, nodeB) => nodeB.e - nodeA.e)
    .map((node) => node.id);

  allNodeIds.forEach((id, index) => {
    allNodeIdsLookup[id] = {
      // Ranking of node by size
      index,

      // Availability of node to be filtered by the slider
      sliderAvailability: true,

      // Check for whether node should be filtered from the chart
      filterState: true,

      // If a family checkbox is unchecked, this state ensures nodes won't be expanded
      // back in if they have already been removed by the other filters
      familyCheckbox: true,
    };
  });

  // Add family property to the lookup
  savedChartItems.forEach((item) => {
    if (item.type === "node") {
      allNodeIdsLookup[item.id].family = item.d.family;
    }
  });
}

// Helper function to normalize degrees score
function getResizeArray(values) {
  const valuesArray = Object.values(values);
  const max = Math.max.apply(null, valuesArray);
  const min = Math.min.apply(null, valuesArray);
  const resizeArray = Object.keys(values).map((id) => ({
    id,
    e: ((values[id] - min) / (max - min)) * 2 + 1,
  }));
  return resizeArray;
}

async function setNodesSize() {
  const degreeScores = await visibleChart.graph().degrees({ value: "total" });
  const resizeValues = getResizeArray(degreeScores);
  visibleChart.setProperties(resizeValues);

  // Save the chart to retrieve nodes sizes when filtering them back in.
  savedChartItems = visibleChart.serialize().items;
  return resizeValues;
}

async function startKeyLines() {
  const options = {
    logo: {
      u: "/public/images/Logo.png",
    },
    selectedNode: {
      ha0: {
        c: "#5d81f8",
        r: 35,
        w: 15,
      },
    },
    selectedLink: {},
  };

  [visibleChart, hiddenChart] = await KeyLines.create([
    { container: "klchart", options },
    { container: "hiddenChart" },
  ]);

  visibleChart.load(data);
  hiddenChart.load(data);

  // Set the size of each node by normalized degree score
  const nodesSizedByDegree = await setNodesSize();

  // Set the order of nodes ids to be filtered for the slider
  setNodesIdsLookup(nodesSizedByDegree);

  // Set nodes to be filtered by the slider
  setIdsForSliderStack();

  visibleChart.layout("organic", { consistent: true, packing: "adaptive" });
  initialiseInteractions();
  setLargestComponentSize();
}

window.addEventListener("DOMContentLoaded", startKeyLines);
export const data = {
  type: "LinkChart",
  items: [
    {
      id: "14-101",
      id1: "14",
      id2: "101",
      d: {
        meetings: 0,
        phonecalls: 2,
        total: 2,
      },
      type: "link",
      w: 3,
    },
    {
      id: "18-19",
      id1: "18",
      id2: "19",
      d: {
        meetings: 2,
        phonecalls: 11,
        total: 13,
      },
      type: "link",
      w: 19.5,
    },
    {
      id: "102-18",
      id1: "102",
      id2: "18",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "32-18",
      id1: "32",
      id2: "18",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "103-18",
      id1: "103",
      id2: "18",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "104-18",
      id1: "104",
      id2: "18",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "22-31",
      id1: "22",
      id2: "31",
      d: {
        meetings: 2,
        phonecalls: 1,
        total: 3,
      },
      type: "link",
      w: 4.5,
    },
    {
      id: "22-19",
      id1: "22",
      id2: "19",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "61-105",
      id1: "61",
      id2: "105",
      d: {
        meetings: 0,
        phonecalls: 2,
        total: 2,
      },
      type: "link",
      w: 3,
    },
    {
      id: "18-61",
      id1: "18",
      id2: "61",
      d: {
        meetings: 0,
        phonecalls: 2,
        total: 2,
      },
      type: "link",
      w: 3,
    },
    {
      id: "33-18",
      id1: "33",
      id2: "18",
      d: {
        meetings: 0,
        phonecalls: 6,
        total: 6,
      },
      type: "link",
      w: 9,
    },
    {
      id: "18-21",
      id1: "18",
      id2: "21",
      d: {
        meetings: 1,
        phonecalls: 3,
        total: 4,
      },
      type: "link",
      w: 6,
    },
    {
      id: "108-18",
      id1: "108",
      id2: "18",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "109-58",
      id1: "109",
      id2: "58",
      d: {
        meetings: 0,
        phonecalls: 3,
        total: 3,
      },
      type: "link",
      w: 4.5,
    },
    {
      id: "110-109",
      id1: "110",
      id2: "109",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "75-58",
      id1: "75",
      id2: "58",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "109-111",
      id1: "109",
      id2: "111",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "75-112",
      id1: "75",
      id2: "112",
      d: {
        meetings: 0,
        phonecalls: 5,
        total: 5,
      },
      type: "link",
      w: 7.5,
    },
    {
      id: "75-64",
      id1: "75",
      id2: "64",
      d: {
        meetings: 0,
        phonecalls: 3,
        total: 3,
      },
      type: "link",
      w: 4.5,
    },
    {
      id: "75-113",
      id1: "75",
      id2: "113",
      d: {
        meetings: 0,
        phonecalls: 3,
        total: 3,
      },
      type: "link",
      w: 4.5,
    },
    {
      id: "75-114",
      id1: "75",
      id2: "114",
      d: {
        meetings: 0,
        phonecalls: 2,
        total: 2,
      },
      type: "link",
      w: 3,
    },
    {
      id: "75-115",
      id1: "75",
      id2: "115",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "75-116",
      id1: "75",
      id2: "116",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "117-61",
      id1: "117",
      id2: "61",
      d: {
        meetings: 0,
        phonecalls: 2,
        total: 2,
      },
      type: "link",
      w: 3,
    },
    {
      id: "80-61",
      id1: "80",
      id2: "61",
      d: {
        meetings: 0,
        phonecalls: 3,
        total: 3,
      },
      type: "link",
      w: 4.5,
    },
    {
      id: "99-23",
      id1: "99",
      id2: "23",
      d: {
        meetings: 0,
        phonecalls: 5,
        total: 5,
      },
      type: "link",
      w: 7.5,
    },
    {
      id: "99-118",
      id1: "99",
      id2: "118",
      d: {
        meetings: 0,
        phonecalls: 3,
        total: 3,
      },
      type: "link",
      w: 4.5,
    },
    {
      id: "27-47",
      id1: "27",
      id2: "47",
      d: {
        meetings: 6,
        phonecalls: 5,
        total: 11,
      },
      type: "link",
      w: 16.5,
    },
    {
      id: "119-27",
      id1: "119",
      id2: "27",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "43-120",
      id1: "43",
      id2: "120",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "43-47",
      id1: "43",
      id2: "47",
      d: {
        meetings: 3,
        phonecalls: 5,
        total: 8,
      },
      type: "link",
      w: 12,
    },
    {
      id: "47-121",
      id1: "47",
      id2: "121",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "47-36",
      id1: "47",
      id2: "36",
      d: {
        meetings: 3,
        phonecalls: 2,
        total: 5,
      },
      type: "link",
      w: 7.5,
    },
    {
      id: "45-40",
      id1: "45",
      id2: "40",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "47-122",
      id1: "47",
      id2: "122",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "47-49",
      id1: "47",
      id2: "49",
      d: {
        meetings: 1,
        phonecalls: 2,
        total: 3,
      },
      type: "link",
      w: 4.5,
    },
    {
      id: "47-48",
      id1: "47",
      id2: "48",
      d: {
        meetings: 8,
        phonecalls: 4,
        total: 12,
      },
      type: "link",
      w: 18,
    },
    {
      id: "47-123",
      id1: "47",
      id2: "123",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "96-47",
      id1: "96",
      id2: "47",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "54-47",
      id1: "54",
      id2: "47",
      d: {
        meetings: 1,
        phonecalls: 4,
        total: 5,
      },
      type: "link",
      w: 7.5,
    },
    {
      id: "47-45",
      id1: "47",
      id2: "45",
      d: {
        meetings: 9,
        phonecalls: 7,
        total: 16,
      },
      type: "link",
      w: 24,
    },
    {
      id: "47-51",
      id1: "47",
      id2: "51",
      d: {
        meetings: 5,
        phonecalls: 4,
        total: 9,
      },
      type: "link",
      w: 13.5,
    },
    {
      id: "52-124",
      id1: "52",
      id2: "124",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "56-125",
      id1: "56",
      id2: "125",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "56-49",
      id1: "56",
      id2: "49",
      d: {
        meetings: 0,
        phonecalls: 2,
        total: 2,
      },
      type: "link",
      w: 3,
    },
    {
      id: "97-27",
      id1: "97",
      id2: "27",
      d: {
        meetings: 1,
        phonecalls: 2,
        total: 3,
      },
      type: "link",
      w: 4.5,
    },
    {
      id: "27-126",
      id1: "27",
      id2: "126",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "58-18",
      id1: "58",
      id2: "18",
      d: {
        meetings: 1,
        phonecalls: 5,
        total: 6,
      },
      type: "link",
      w: 9,
    },
    {
      id: "127-18",
      id1: "127",
      id2: "18",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "61-66",
      id1: "61",
      id2: "66",
      d: {
        meetings: 1,
        phonecalls: 9,
        total: 10,
      },
      type: "link",
      w: 15,
    },
    {
      id: "18-66",
      id1: "18",
      id2: "66",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "61-67",
      id1: "61",
      id2: "67",
      d: {
        meetings: 0,
        phonecalls: 5,
        total: 5,
      },
      type: "link",
      w: 7.5,
    },
    {
      id: "129-61",
      id1: "129",
      id2: "61",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "128-61",
      id1: "128",
      id2: "61",
      d: {
        meetings: 0,
        phonecalls: 6,
        total: 6,
      },
      type: "link",
      w: 9,
    },
    {
      id: "75-61",
      id1: "75",
      id2: "61",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "61-130",
      id1: "61",
      id2: "130",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "131-61",
      id1: "131",
      id2: "61",
      d: {
        meetings: 0,
        phonecalls: 4,
        total: 4,
      },
      type: "link",
      w: 6,
    },
    {
      id: "18-29",
      id1: "18",
      id2: "29",
      d: {
        meetings: 4,
        phonecalls: 15,
        total: 19,
      },
      type: "link",
      w: 28.5,
    },
    {
      id: "18-132",
      id1: "18",
      id2: "132",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "133-61",
      id1: "133",
      id2: "61",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "29-54",
      id1: "29",
      id2: "54",
      d: {
        meetings: 1,
        phonecalls: 1,
        total: 2,
      },
      type: "link",
      w: 3,
    },
    {
      id: "29-64",
      id1: "29",
      id2: "64",
      d: {
        meetings: 5,
        phonecalls: 1,
        total: 6,
      },
      type: "link",
      w: 9,
    },
    {
      id: "29-63",
      id1: "29",
      id2: "63",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "134-61",
      id1: "134",
      id2: "61",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "18-125",
      id1: "18",
      id2: "125",
      d: {
        meetings: 0,
        phonecalls: 2,
        total: 2,
      },
      type: "link",
      w: 3,
    },
    {
      id: "135-47",
      id1: "135",
      id2: "47",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "47-29",
      id1: "47",
      id2: "29",
      d: {
        meetings: 2,
        phonecalls: 1,
        total: 3,
      },
      type: "link",
      w: 4.5,
    },
    {
      id: "43-29",
      id1: "43",
      id2: "29",
      d: {
        meetings: 4,
        phonecalls: 6,
        total: 10,
      },
      type: "link",
      w: 15,
    },
    {
      id: "27-51",
      id1: "27",
      id2: "51",
      d: {
        meetings: 4,
        phonecalls: 1,
        total: 5,
      },
      type: "link",
      w: 7.5,
    },
    {
      id: "29-51",
      id1: "29",
      id2: "51",
      d: {
        meetings: 2,
        phonecalls: 2,
        total: 4,
      },
      type: "link",
      w: 6,
    },
    {
      id: "136-27",
      id1: "136",
      id2: "27",
      d: {
        meetings: 0,
        phonecalls: 2,
        total: 2,
      },
      type: "link",
      w: 3,
    },
    {
      id: "50-47",
      id1: "50",
      id2: "47",
      d: {
        meetings: 10,
        phonecalls: 2,
        total: 12,
      },
      type: "link",
      w: 18,
    },
    {
      id: "68-47",
      id1: "68",
      id2: "47",
      d: {
        meetings: 2,
        phonecalls: 5,
        total: 7,
      },
      type: "link",
      w: 10.5,
    },
    {
      id: "77-137",
      id1: "77",
      id2: "137",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "69-77",
      id1: "69",
      id2: "77",
      d: {
        meetings: 0,
        phonecalls: 3,
        total: 3,
      },
      type: "link",
      w: 4.5,
    },
    {
      id: "77-58",
      id1: "77",
      id2: "58",
      d: {
        meetings: 0,
        phonecalls: 2,
        total: 2,
      },
      type: "link",
      w: 3,
    },
    {
      id: "54-69",
      id1: "54",
      id2: "69",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {
      id: "68-54",
      id1: "68",
      id2: "54",
      d: {
        meetings: 1,
        phonecalls: 3,
        total: 4,
      },
      type: "link",
      w: 6,
    },
    {
      id: "68-45",
      id1: "68",
      id2: "45",
      d: {
        meetings: 4,
        phonecalls: 3,
        total: 7,
      },
      type: "link",
      w: 10.5,
    },
    {
      id: "69-45",
      id1: "69",
      id2: "45",
      d: {
        meetings: 0,
        phonecalls: 2,
        total: 2,
      },
      type: "link",
      w: 3,
    },
    {
      id: "51-45",
      id1: "51",
      id2: "45",
      d: {
        meetings: 5,
        phonecalls: 1,
        total: 6,
      },
      type: "link",
      w: 9,
    },
    {
      id: "27-68",
      id1: "27",
      id2: "68",
      d: {
        meetings: 5,
        phonecalls: 1,
        total: 6,
      },
      type: "link",
      w: 9,
    },
    {
      id: "138-27",
      id1: "138",
      id2: "27",
      d: {
        meetings: 0,
        phonecalls: 1,
        total: 1,
      },
      type: "link",
      w: 1.5,
    },
    {

// ...truncated 5410 lines
<!doctype html>
<html lang="en" style="background-color: #2d383f">
  <head>
    <meta charset="utf-8" />
    <title>Mafia Network</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" href="./style.css" />
  </head>
  <body>
    <div id="klchart" class="klchart"></div>
    <div style="display: none" id="hiddenChart"></div>
    <script type="module" src="./code.js"></script>
  </body>
</html>
.sliderContainer {
  width: 100%;
}

dl {
  margin: 0px;
}

dl dt {
  color: #fff;
  float: left;
  font-weight: bold;
  margin-left: 0px;
  margin-right: 10px;
  margin-block-start: 0em;
  padding: 3px;
  width: 22px;
  height: 22px;
  border-radius: 22px;
  line-height: 12px;
}

dl dd {
  margin: 2px 0;
  padding: 5px 0;
  line-height: 12px;
  font-size: 12px;
}

.batanesi dt {
  background-color: #8e9542;
}

.mistretta dt {
  background-color: #a04b6d;
}

.none dt {
  background-color: white;
}

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.