Search

Social Media Analysis

Analysis

Explore connections between social media accounts.

Social Media Analysis
View live example →

This demo shows a fictional group of people linked by their activity on various social media platforms.

When analysing connections, it is useful to resolve multiple identifiers (such as social media accounts) into a single entity (the account owner). This reveals interesting patterns without overwhelming the chart with the clutter of individual communications.

Combos can represent a single entity, but also enable analysts to drill down into detail and explore how information spreads between groups of people.

The 𝕏 logo is a trademark of X, Inc.

The ** logo is a trademark of Meta.

Key functions used:

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

let chart;
let hoveredGlyph = null;

const expandLayoutOpts = {
  fit: true,
  name: "organic",
  mode: "adaptive",
};

function getGlyphs(item) {
  const isOpenCombo = chart.combo().isOpen(item.id);
  return isOpenCombo ? item.oc.g : item.g;
}

function setGlyphs(id, glyphs, isOpenCombo = chart.combo().isOpen(id)) {
  const g = isOpenCombo ? { oc: { g: glyphs } } : { g: glyphs };
  chart.setProperties({ id, ...g });
}

function getHoveredGlyph(item, subId) {
  if (item) {
    if (item.d.type === "post" && item.g && item.g.length === 1) {
      return 0;
    }
    const glyphs = getGlyphs(item);
    if (glyphs) {
      return glyphs.findIndex((g) => g.p.toString() === subId);
    }
  }
}

function chartHoverHandler({ id, subItem: { subId } }) {
  const item = chart.getItem(id);
  const index = getHoveredGlyph(item, subId);
  const isOpenCombo = chart.combo().isOpen(id);

  let glyphToHover = null;
  // Work out if the cursor is over a glyph which is in the foreground to focus
  // i.e. over an expand glyph on a node or a post node with an expand glyph
  if (id && item && !item.bg && item.type === "node" && index >= 0) {
    glyphToHover = item.id;
  }
  // Return any previously focussed glyph to normal
  if (hoveredGlyph && hoveredGlyph.nodeId !== glyphToHover) {
    setGlyphs(
      hoveredGlyph.nodeId,
      hoveredGlyph.originalGlyphs,
      hoveredGlyph.isOpenCombo,
    );
    hoveredGlyph = null;
  }
  // Focus any newly hovered glyph
  if (!hoveredGlyph && glyphToHover) {
    const originalGlyphs = getGlyphs(item);
    const newGlyphs = originalGlyphs.map((g, i) => {
      return i === index ? { ...g, c: style.hoverStyles[g.c] } : g;
    });

    hoveredGlyph = { nodeId: id, sub: subId, originalGlyphs, isOpenCombo };
    setGlyphs(id, newGlyphs);
  }
}

function getAllCombos() {
  const combos = [];
  // In this demo all top level items are combos and there are no nested combos
  chart.each({ type: "node", items: "toplevel" }, (combo) => {
    combos.push(combo.id);
  });
  return combos;
}

function enableInput(enabled) {
  const buttons = ["openCombos", "closeCombos", "expandCombos"];
  buttons.forEach((id) => {
    const button = document.getElementById(id);
    if (enabled) {
      button.classList.remove("disabled");
      button.removeAttribute("disabled");
    } else {
      button.classList.add("disabled");
      button.setAttribute("disabled", "");
    }
  });
}

async function openCombos() {
  enableInput(false);
  await chart.combo().open(getAllCombos());
  chart.layout("organic", { mode: "adaptive" });
  enableInput(true);
}

async function closeCombos() {
  enableInput(false);
  await chart.combo().close(getAllCombos());
  chart.layout("organic", { mode: "adaptive" });
  enableInput(true);
}

async function expandAllCombos() {
  enableInput(false);
  let allOwners = [];
  for (const owner in data.rawData.owners) {
    const combo = createEntityCombo(owner);
    allOwners.push(...combo);
  }

  await chart.expand(allOwners, {
    layout: expandLayoutOpts,
    time: 700,
    arrange: { name: "lens" },
  });
  styleLinks();
  enableInput(true);
}

function getRepostIds(platformInfo, postId) {
  return data.rawData[platformInfo.data]
    .filter((p) => p[platformInfo.copyOf] === postId)
    .map((p) => p.id);
}

/* Create chart items */
function createPostNode(platformInfo, post, owner) {
  const reposts = getRepostIds(platformInfo, post.id);
  const colour =
    post[platformInfo.copyOf] || reposts.length > 0
      ? platformInfo.colour
      : platformInfo.fadedColour;

  return {
    id: post.id,
    type: "node",
    fi: { t: platformInfo.postIcon, c: colour },
    e: 0.75,
    d: {
      type: "post",
      platform: platformInfo.name,
      idx: post.idx,
      copyOf: post[platformInfo.copyOf],
      reposts,
    },
    parentId: owner,
  };
}

function createAuthoredByLink(platformInfo, post) {
  return {
    id: `${platformInfo.author}${post[platformInfo.author]} ${post.id}`,
    type: "link",
    id1: post[platformInfo.author],
    id2: post.id,
    a2: true,
    c: platformInfo.colour,
    d: { type: "authoredBy" },
  };
}

function createRepostLink(platformInfo, repostId, postId) {
  return {
    id: repostId + platformInfo.copyOf + postId,
    type: "link",
    id1: repostId,
    id2: postId,
    a1: true,
    c: platformInfo.colour,
    d: { type: "repost" },
  };
}

function createAccountNodes(owner, platformInfo) {
  const ownerInfo = data.rawData.owners[owner];

  const accountIcon =
    platformInfo.name === "x"
      ? "/public/images/icons/x-logo-2023.svg"
      : "/public/images/icons/facebook-2023.svg";

  return ownerInfo[`${platformInfo.name}Accounts`].map((account) => ({
    id: account.id,
    type: "node",
    c: platformInfo.name === "x" ? "rgb(255, 255, 255)" : "transparent",
    bw: 2,
    b: "transparent",
    e: 0.8,
    fc: style.textColour,
    fs: 16,
    t: [
      {
        t: account.name,
        borderRadius: 20,
        bw: 0,
        padding: "3 5 0 5",
        fbc: "rgba(255, 255, 255, 0)",
        b: "rgb(255, 255, 255)",
      },
    ],
    u: accountIcon,
    parentId: owner,
    d: { type: "account", platform: platformInfo.name },
  }));
}

function getPlatformContentBy(owner, platform, links) {
  const platformInfo = style.platformDetails[platform];
  const platformPosts = data.rawData[platformInfo.data];
  const accountNodes = createAccountNodes(owner, platformInfo);
  const accounts = accountNodes.map((node) => node.id);
  const posts = platformPosts
    .filter((post) => accounts.includes(post[platformInfo.author]))
    .map((post) => {
      const node = createPostNode(platformInfo, post, owner);
      links.push(createAuthoredByLink(platformInfo, post));
      // Expand in all the copyOf links for this post
      // If the other end isn't in the chart yet, they will automatically be filtered out
      if (post[platformInfo.copyOf]) {
        links.push(
          createRepostLink(platformInfo, post.id, post[platformInfo.copyOf]),
        );
      }
      node.d.reposts.forEach((repost) => {
        links.push(createRepostLink(platformInfo, repost, node.id));
      });
      return node;
    });
  return [...accountNodes, ...posts];
}
function getLinkSize(id) {
  if (chart.combo().isCombo(id, { type: "link" })) {
    // set the link thickness
    return 3.5 * Math.sqrt(chart.combo().info(id).links.length);
  }
  return 3.5;
}

function createEntityCombo(owner) {
  const ownerInfo = data.rawData.owners[owner];
  const ownerImage = `/public/im/people/${owner}.png`;
  const entityGlyph = {
    u: ownerImage,
    p: style.entityGlyphPos,
    e: 3,
  };

  const label = {
    t: `${ownerInfo.firstname} ${ownerInfo.surname}`,
    fs: 16,
    borderRadius: 8,
    bw: 0,
    b: "transparent",
    fc: style.textColour,
    fbc: "transparent",
    position: "s",
    padding: "3 5 0 5",
  };

  let nodes = [
    {
      id: owner,
      type: "node",
      e: 2,
      fc: style.textColour,
      fi: style.entityIcon,
      fs: 16,
      t: label,
      u: ownerImage,
      oc: {
        bw: 4,
        c: "rgba(21, 6, 28, 0.5)",
        b: "rgb(255, 255, 255)",
        g: [entityGlyph, style.closeComboGlyph],
        t: label,
      },
      g: [style.openComboGlyph],
      d: { type: "owner" },
    },
  ];
  const links = [];
  ["x", "fb"].forEach((platform) => {
    nodes = nodes.concat(getPlatformContentBy(owner, platform, links));
  });

  return [...nodes, ...links];
}

/* END of functions to create chart items */

// Expand in the neighbours of item not already in the chart
// and if highlight is true, highlight all the neighbours of item
function getRepostsToExpandIn(postId, postData) {
  let items = [];
  const platformInfo = style.platformDetails[postData.platform];
  const allPosts = data.rawData[platformInfo.data];
  const reposts = postData.reposts.map((pId) => {
    const id = pId.slice(platformInfo.identifier.length);
    return { id, dir: "to" };
  });

  if (postData.copyOf) {
    reposts.push({
      id: postData.copyOf.slice(platformInfo.identifier.length),
      dir: "from",
    });
  }
  // Repost already in the chart => nothing to expand in => filter out
  reposts
    .filter((repost) => !chart.getItem(platformInfo.identifier + repost.id))
    .forEach((repost) => {
      const p = allPosts[repost.id];
      const author = p[platformInfo.author];
      const owner = data.rawData.accounts[postData.platform][author].owner;

      // Include all the accounts of this owner
      items = items.concat(createEntityCombo(owner));

      const origPost = repost.dir === "from" ? p.id : postId;
      const repostedTo = repost.id === "from" ? postId : p.id;
      const repostLink = createRepostLink(platformInfo, repostedTo, origPost);

      items.push(repostLink);
    });
  styleLinks();
  return items;
}

// Update the expand glyphs on current chart items to correctly identify which can be expanded
function updateGlyphs() {
  const props = [];
  const expandableCombos = {};
  const graphEngine = KeyLines.getGraphEngine();
  graphEngine.load(chart.serialize());
  const degrees = graphEngine.degrees();

  chart.each({ type: "node" }, (node) => {
    if (node.d && node.d.type === "post") {
      let numReposts = node.d.reposts ? node.d.reposts.length : 0;
      if (node.d.copyOf) numReposts++;
      if (numReposts > 0) {
        const inChart = degrees[node.id] - 1;
        if (inChart !== numReposts) {
          props.push({ id: node.id, g: [style.expandGlyph] });

          const parentCombo = chart.getItem(node.parentId);

          props.push({
            id: node.parentId,
            g: [style.openComboGlyph, style.expandGlyph],
            oc: {
              ...parentCombo.oc,
              g: [...parentCombo.oc.g, style.expandGlyph],
            },
          });
          expandableCombos[node.parentId] = true;
        } else {
          props.push({ id: node.id, g: [] });
        }
      }
    }
  });

  // Check if glyph needs removing from any combos
  chart.each({ type: "node", items: "toplevel" }, (combo) => {
    if (!expandableCombos[combo.id]) {
      props.push({
        id: combo.id,
        g: [combo.g[0]],
        oc: { g: combo.oc.g.slice(0, 2) },
      });
    }
  });
  chart.setProperties(props);
}

async function loadStartingNode() {
  // data is defined in socialmedia-data.js
  chart.clear();
  const startingCombo = createEntityCombo(data.initialEntity);
  await chart.expand(startingCombo, {
    layout: expandLayoutOpts,
    time: 200,
    arrange: { name: "lens" },
  });

  exploreOutwards(startingCombo[0]);

  chart.selection([startingCombo[0].id]);
  updateGlyphs();
  styleLabels(chart.selection());
  styleNodes(chart.selection());
  styleLinks();
}

/**
 * Get the items to expand into the chart so that all direct reposts or previous posts of
 * postId (the data about which is in the postData object) are included in the chart
 * (including the details of their containing combos)
 */
function getAllCopiesOfPost(postId) {
  let origPostId = postId;
  let origPost = chart.getItem(postId);

  while (origPost.d.copyOf) {
    origPost = chart.getItem(origPost.d.copyOf);
    // Only store ID and continue search for original if node in the chart
    if (origPost) origPostId = origPost.id;
    else break;
  }

  let reposts = [origPostId];
  for (let i = 0; i < reposts.length; i++) {
    const repost = chart.getItem(reposts[i]);
    if (repost) {
      reposts = [...reposts, ...repost.d.reposts];
    }
  }
  return reposts;
}

function isParentOpen(nodeId) {
  return chart.combo().isOpen(chart.combo().find(nodeId));
}

/**
 * Foreground the item defined by id and its linked items.
 * For owners this is just their neighbours
 * For accounts this is all accounts that have interacted with it and all posts by those accounts
 *  (including the original account)
 * For posts this is all copies of the post and the accounts making those posts
 */
function revealAndForeground(linksToReveal, itemsToForeground, fgItems) {
  const bgLinks = [];

  // Only reveal links if at least one end inside an open combo
  const toReveal = linksToReveal.filter((linkId) => {
    const link = chart.getItem(linkId);
    if (isParentOpen(link.id1) || isParentOpen(link.id2)) {
      // Background any parent link unless it is explicitly set to be foregrounded
      if (
        link.parentId &&
        chart.getItem(link.parentId).type === "link" &&
        !itemsToForeground.includes(link.parentId)
      ) {
        bgLinks.push(link.parentId);
      }
      return true;
    }
    return false;
  });
  chart.combo().reveal(toReveal);
  chart.foreground((item) => itemsToForeground.includes(item.id), {
    type: "all",
    items: fgItems,
  });
  // background all combolinks of revealed links
  chart.setProperties(bgLinks.map((linkId) => ({ id: linkId, bg: true })));
}

function highlightRelations(ids) {
  const clickedNodes = chart
    .getItem(ids)
    .filter((item) => item.type === "node");
  let fgItems;

  if (clickedNodes.length > 0) {
    let nbrs;
    let itemsToForeground = [...ids];
    const linksToReveal = [];

    const graphEngine = KeyLines.getGraphEngine();
    graphEngine.load(chart.serialize());

    clickedNodes.forEach((clicked) => {
      if (clicked.d.type === "owner") {
        fgItems = "toplevel";
        nbrs = chart.graph().neighbours(clicked.id);
        itemsToForeground = itemsToForeground.concat(nbrs.nodes, nbrs.links, [
          clicked.id,
        ]);
      } else {
        fgItems = "underlying";
        if (clicked.d.type === "account") {
          // Get the accounts that have interacted with this account
          const nbrs3 = graphEngine.neighbours(clicked.id, { hops: 3 });
          const connectedAccounts = nbrs3.nodes.filter((nodeId) => {
            const isAccount = chart.getItem(nodeId).d.type === "account";
            return isAccount;
          });
          connectedAccounts.push(clicked.id);

          // Get the posts by these accounts (and links to them)
          nbrs = graphEngine.neighbours(connectedAccounts);

          // Get underlying links between the accounts
          // (these won't be revealed but the combolink will be foregrounded)
          const interAccountLinks = graphEngine.neighbours(clicked.id, {
            hops: 2,
          }).links;
          itemsToForeground = itemsToForeground.concat(
            nbrs.nodes,
            nbrs.links,
            connectedAccounts,
            interAccountLinks,
          );
        } else {
          // clicked.d.type === 'post'
          const copiesOfPost = getAllCopiesOfPost(clicked.id);
          itemsToForeground = [...itemsToForeground, ...copiesOfPost];
          chart.each({ type: "link" }, (link) => {
            // Also foreground accounts which posted these posts
            // (account is always id1 on a postedBy link)
            if (copiesOfPost.includes(link.id2)) {
              if (!copiesOfPost.includes(link.id1)) {
                itemsToForeground.push(link.id1);
              }
              linksToReveal.push(link.id);
            }
          });
          itemsToForeground = [...itemsToForeground, ...linksToReveal];
        }
      }
    });
    revealAndForeground(linksToReveal, itemsToForeground, fgItems);
  } else {
    chart.combo().reveal([]);
    chart.foreground(() => true);
  }
}

async function styleLabels(selections) {
  const props = [];
  const colourLabel = (item, colour) => {
    let labels = item.t;
    if (Array.isArray(labels)) {
      labels = labels.map((label) => ({ ...label, fbc: colour }));
    }

    return { ...item, t: labels };
  };

  chart.each({ type: "node", items: "all" }, (item) => {
    const { id, t, oc } = item;
    const colour = selections.includes(id)
      ? style.selectionColour
      : "transparent";
    props.push({ id, ...colourLabel({ t, oc }, colour) });
  });
  await chart.setProperties(props);
}

function styleNodes(selection) {
  const propertiesToSet = [];

  chart.each({ type: "node", items: "toplevel" }, (node) => {
    let border;

    if (selection.includes(node.id)) {
      // hide border
      border = "transparent";
    } else {
      // add border
      border = "white";
    }

    propertiesToSet.push({
      id: node.id,
      ...node,
      oc: { ...node.oc, t: node.t, b: border },
    });
  });

  chart.setProperties(propertiesToSet);
}

function styleLinks() {
  const propertiesToSet = [];
  chart.each({ type: "link", items: "toplevel" }, (link) => {
    propertiesToSet.push({
      id: link.id,
      ...link,
      w: getLinkSize(link.id),
    });
  });

  chart.setProperties(propertiesToSet);
}

function selectionchangeHandler() {
  const selection = chart.selection();
  if (selection.length === 0) {
    highlightRelations(selection);
    styleLabels(selection);
    styleNodes(selection);
  } else {
    const items = chart.getItem(selection);
    const nodeIds = items
      .filter((item) => item.type !== "link")
      .map((item) => item.id);
    highlightRelations(nodeIds);
    styleLabels(nodeIds);
    styleNodes(nodeIds);
  }
}

async function exploreOutwards(item, highlight) {
  let expandIn = [];
  if (!item || !item.id || item.type !== "node" || !item.d) {
    return;
  }
  if (item.d.type === "post") {
    // Add in reposts
    expandIn = getRepostsToExpandIn(item.id, item.d);
  } else if (item.d.type === "owner") {
    const content = chart.combo().info(item.id).nodes;
    content.forEach((t) => {
      if (t.d.type === "post") {
        expandIn = expandIn.concat(getRepostsToExpandIn(t.id, t.d));
      }
    });
  } else {
    return;
  }
  // Prevent opening/closing of combos while data is being added to the chart
  chart.on("double-click", ({ preventDefault }) => preventDefault());
  await chart.expand(expandIn, {
    layout: expandLayoutOpts,
    arrange: { name: "lens" },
    time: 700,
  });
  chart.off("double-click");
  updateGlyphs();
  if (highlight) highlightRelations([item.id]);
}

function toggleCombo(id) {
  const isCombo = chart.combo().isCombo(id);
  if (isCombo) {
    const isOpen = chart.combo().isOpen(id);
    if (isOpen) {
      chart.combo().close(id);
    } else {
      chart.combo().open(id);
    }
  }
}

function chartClickHandler({ id, subItem: { subId } }) {
  const item = chart.getItem(id);
  // Only check for glyph clicks if in foreground
  if (item && item.type === "node" && !item.bg) {
    if (
      subId === style.expandGlyphPos.toString() ||
      (item.d.type === "post" && item.g && item.g.length === 1)
    ) {
      // Ensure the expand glyph isn't re-added to this node
      if (hoveredGlyph && hoveredGlyph.nodeId === id) {
        hoveredGlyph = null;
      }
      exploreOutwards(item, true);
    } else if (subId === style.openCloseGlyphPos.toString()) {
      toggleCombo(id);
    }
  }
}

function initialiseInteractions() {
  // Setup handlers
  chart.on("hover", chartHoverHandler);
  chart.on("click", chartClickHandler);
  chart.on("selection-change", selectionchangeHandler);
  document.getElementById("openCombos").addEventListener("click", openCombos);
  document.getElementById("closeCombos").addEventListener("click", closeCombos);
  document
    .getElementById("expandCombos")
    .addEventListener("click", expandAllCombos);
  document.getElementById("reset").addEventListener("click", loadStartingNode);

  loadStartingNode();
}

async function startKeyLines() {
  // Set offsets and enlargements for Font Icons
  const imageAlignment = {
    "fas fa-user": { dy: -12, e: 1.2 },
    "fas fa-plus": { e: 1 },
    "/public/images/icons/x-logo-2023.svg": { e: 0.5 },
  };

  const options = {
    arrows: "large",
    defaultStyles: {
      comboGlyph: null,
      comboLinks: { c: "rgb(201, 160, 217)" },
    },
    handMode: true,
    navigation: false,
    minZoom: -4,
    hover: 0,
    iconFontFamily: "Font Awesome 5 Brands",
    imageAlignment,
    selectionColour: style.selectionColour,
    selectedLink: {},
    gradient: {
      stops: [
        { c: "#543c5e", r: 0 },
        { c: "#15001f", r: 1 },
      ],
    },
  };

  chart = await KeyLines.create({ container: "klchart", options });

  initialiseInteractions();
}

function loadKeyLines() {
  Promise.all([
    document.fonts.load('24px "Font Awesome 5 Free"'),
    document.fonts.load('24px "Font Awesome 5 Brands"'),
  ]).then(startKeyLines);
}

window.addEventListener("DOMContentLoaded", loadKeyLines);
import KeyLines from "keylines";
const data = {
  rawData: {
    owners: {
      Person0: {
        idx: 0,
        id: "Person0",
        firstname: "Diane",
        surname: "Denman",
        nickname: "Di",
        fbAccounts: [
          {
            id: "Person0fb0",
            name: "Di D",
            owner: "Person0",
            reposts: [1, 2, 3],
            platform: "fb",
          },
        ],
        xAccounts: [
          {
            id: "Person0x0",
            name: "@Di",
            owner: "Person0",
            reposts: [0, 1, 9],
            platform: "x",
          },
          {
            id: "Person0x1",
            name: "@Di D",
            owner: "Person0",
            reposts: [0, 1, 9],
            platform: "x",
          },
        ],
      },
      Person1: {
        idx: 1,
        id: "Person1",
        firstname: "Ophelia",
        surname: "Winston",
        fbAccounts: [
          {
            id: "Person1fb0",
            name: "Ophelia W",
            owner: "Person1",
            reposts: [0, 2, 3, 12, 13, 14],
            platform: "fb",
          },
        ],
        xAccounts: [
          {
            id: "Person1x0",
            name: "@Ophelia",
            owner: "Person1",
            reposts: [0, 2, 12, 13, 14],
            platform: "x",
          },
        ],
      },
      Person2: {
        idx: 2,
        id: "Person2",
        firstname: "Niall",
        surname: "Milligan",
        fbAccounts: [
          {
            id: "Person2fb0",
            name: "Niall M",
            owner: "Person2",
            reposts: [0, 1, 3],
            platform: "fb",
          },
        ],
        xAccounts: [
          {
            id: "Person2x0",
            name: "@Niall",
            owner: "Person2",
            reposts: [0, 1, 2],
            platform: "x",
          },
          {
            id: "Person2x1",
            name: "@Niall M",
            owner: "Person2",
            reposts: [0, 1, 2],
            platform: "x",
          },
        ],
      },
      Person3: {
        idx: 3,
        id: "Person3",
        firstname: "Emanuela",
        surname: "Cooper",
        nickname: "Ella",
        fbAccounts: [
          {
            id: "Person3fb0",
            name: "Ella C",
            owner: "Person3",
            reposts: [0, 1, 2, 4, 5],
            platform: "fb",
          },
        ],
        xAccounts: [],
      },
      Person4: {
        idx: 4,
        id: "Person4",
        firstname: "Tammy",
        surname: "McKendrick",
        fbAccounts: [
          {
            id: "Person4fb0",
            name: "Tammy McK",
            owner: "Person4",
            reposts: [3, 5],
            platform: "fb",
          },
        ],
        xAccounts: [],
      },
      Person5: {
        idx: 5,
        id: "Person5",
        firstname: "Malakai",
        surname: "Janson",
        fbAccounts: [
          {
            id: "Person5fb0",
            name: "Malakai J",
            owner: "Person5",
            reposts: [3, 4],
            platform: "fb",
          },
        ],
        xAccounts: [],
      },
      Person6: {
        idx: 6,
        id: "Person6",
        firstname: "Delia",
        surname: "Foster",
        fbAccounts: [
          {
            id: "Person6fb0",
            name: "Delia F",
            owner: "Person6",
            reposts: [4],
            platform: "fb",
          },
        ],
        xAccounts: [
          {
            id: "Person6x0",
            name: "@Delia",
            owner: "Person6",
            reposts: [6, 7],
            platform: "x",
          },
          {
            id: "Person6x1",
            name: "@Delia F",
            owner: "Person6",
            reposts: [6, 7],
            platform: "x",
          },
        ],
      },
      Person7: {
        idx: 7,
        id: "Person7",
        firstname: "Christie",
        surname: "Sergeant",
        nickname: "Chrissie",
        fbAccounts: [],
        xAccounts: [
          {
            id: "Person7x0",
            name: "@Chrissie",
            owner: "Person7",
            reposts: [6],
            platform: "x",
          },
        ],
      },
      Person8: {
        idx: 8,
        id: "Person8",
        firstname: "Coreen",
        surname: "Reis",
        fbAccounts: [],
        xAccounts: [
          {
            id: "Person8x0",
            name: "@Coreen",
            owner: "Person8",
            reposts: [6, 7, 8],
            platform: "x",
          },
          {
            id: "Person8x1",
            name: "@Coreen R",
            owner: "Person8",
            reposts: [6, 7, 8],
            platform: "x",
          },
        ],
      },
      Person9: {
        idx: 9,
        id: "Person9",
        firstname: "Vito",
        surname: "Morello",
        fbAccounts: [],
        xAccounts: [
          {
            id: "Person9x0",
            name: "@Vito",
            owner: "Person9",
            reposts: [0, 9, 10, 11],
            platform: "x",
          },
          {
            id: "Person9x1",
            name: "@Vito M",
            owner: "Person9",
            reposts: [0, 9, 10, 11],
            platform: "x",
          },
        ],
      },
      Person10: {
        idx: 10,
        id: "Person10",
        firstname: "Sebastian",
        surname: "Fabian",
        nickname: "Seb",
        fbAccounts: [],
        xAccounts: [
          {
            id: "Person10x0",
            name: "@Seb",
            owner: "Person10",
            reposts: [9, 11],
            platform: "x",
          },
        ],
      },
      Person11: {
        idx: 11,
        id: "Person11",
        firstname: "Jill",
        surname: "Bukoski",
        fbAccounts: [],
        xAccounts: [
          {
            id: "Person11x0",
            name: "@Jill",
            owner: "Person11",
            reposts: [0, 9, 10],
            platform: "x",
          },
        ],
      },
      Person12: {
        idx: 12,
        id: "Person12",
        firstname: "Ashlea",
        surname: "Coeman",
        fbAccounts: [
          {
            id: "Person12fb0",
            name: "Ashlea C",
            owner: "Person12",
            reposts: [1, 13, 14],
            platform: "fb",
          },
        ],
        xAccounts: [
          {
            id: "Person12x0",
            name: "@Ashlea",
            owner: "Person12",
            reposts: [1, 13, 14],
            platform: "x",
          },
        ],
      },
      Person13: {
        idx: 13,
        id: "Person13",
        firstname: "Valerie",
        surname: "Beaumont",
        nickname: "Val",
        fbAccounts: [
          {
            id: "Person13fb0",
            name: "Val B",
            owner: "Person13",
            reposts: [1, 12, 14],
            platform: "fb",
          },
        ],
        xAccounts: [
          {
            id: "Person13x0",
            name: "@Val",
            owner: "Person13",
            reposts: [1, 12, 14],
            platform: "x",
          },
        ],
      },
      Person14: {
        idx: 14,
        id: "Person14",
        firstname: "Tatiana",
        surname: "Columbo",
        fbAccounts: [
          {
            id: "Person14fb0",
            name: "Tatiana C",
            owner: "Person14",
            reposts: [1, 12, 13, 15],
            platform: "fb",
          },
        ],
        xAccounts: [
          {
            id: "Person14x0",
            name: "@Tatiana",
            owner: "Person14",
            reposts: [1, 12, 13, 14],
            platform: "x",
          },
          {
            id: "Person14x1",
            name: "@Tatiana C",
            owner: "Person14",
            reposts: [1, 12, 13, 14],
            platform: "x",
          },
        ],
      },
      Person15: {
        idx: 15,
        id: "Person15",
        firstname: "Raegan",
        surname: "Ready",
        fbAccounts: [
          {
            id: "Person15fb0",
            name: "Raegan R",
            owner: "Person15",
            reposts: [12, 14, 16, 17],
            platform: "fb",
          },
        ],
        xAccounts: [],
      },
      Person16: {
        idx: 16,
        id: "Person16",
        firstname: "Richard",
        surname: "Gallo",
        nickname: "Ricky",
        fbAccounts: [
          {
            id: "Person16fb0",
            name: "Ricky G",
            owner: "Person16",
            reposts: [],
            platform: "fb",
          },
        ],
        xAccounts: [
          {
            id: "Person16x0",
            name: "@Ricky",
            owner: "Person16",
            reposts: [],
            platform: "x",
          },
        ],
      },
      Person17: {
        idx: 17,
        id: "Person17",
        firstname: "Madilyn",
        surname: "Both",
        nickname: "Maddy",
        fbAccounts: [
          {
            id: "Person17fb0",
            name: "Maddy B",
            owner: "Person17",
            reposts: [15, 16],
            platform: "fb",
          },
        ],
        xAccounts: [
          {
            id: "Person17x0",
            name: "@Maddy",
            owner: "Person17",
            reposts: [15, 16],
            platform: "x",
          },
        ],
      },
      Person18: {
        idx: 18,
        id: "Person18",
        firstname: "Michael",
        surname: "Burnham",
        nickname: "Mike",
        fbAccounts: [
          {
            id: "Person18fb0",
            name: "Mike B",
            owner: "Person18",
            reposts: [2, 19],
            platform: "fb",
          },
        ],
        xAccounts: [],
      },
      Person19: {
        idx: 19,
        id: "Person19",
        firstname: "David",
        surname: "Van Kann",
        nickname: "Dave",
        fbAccounts: [
          {
            id: "Person19fb0",
            name: "Dave Van K",
            owner: "Person19",
            reposts: [18],
            platform: "fb",
          },
        ],
        xAccounts: [],
      },
    },
    accounts: {
      fb: {
        Person0fb0: {
          id: "Person0fb0",
          name: "Di D",
          owner: "Person0",
          reposts: [1, 2, 3],
          platform: "fb",
        },
        Person1fb0: {
          id: "Person1fb0",
          name: "Ophelia W",
          owner: "Person1",
          reposts: [0, 2, 3, 12, 13, 14],
          platform: "fb",
        },
        Person2fb0: {
          id: "Person2fb0",
          name: "Niall M",
          owner: "Person2",
          reposts: [0, 1, 3],
          platform: "fb",
        },
        Person3fb0: {
          id: "Person3fb0",
          name: "Ella C",
          owner: "Person3",
          reposts: [0, 1, 2, 4, 5],
          platform: "fb",
        },
        Person4fb0: {
          id: "Person4fb0",
          name: "Tammy McK",
          owner: "Person4",
          reposts: [3, 5],
          platform: "fb",
        },
        Person5fb0: {
          id: "Person5fb0",
          name: "Malakai J",
          owner: "Person5",
          reposts: [3, 4],
          platform: "fb",
        },
        Person6fb0: {
          id: "Person6fb0",
          name: "Delia F",
          owner: "Person6",
          reposts: [4],
          platform: "fb",
        },
        Person12fb0: {
          id: "Person12fb0",
          name: "Ashlea C",
          owner: "Person12",
          reposts: [1, 13, 14],
          platform: "fb",
        },
        Person13fb0: {
          id: "Person13fb0",
          name: "Val B",
          owner: "Person13",
          reposts: [1, 12, 14],
          platform: "fb",
        },
        Person14fb0: {
          id: "Person14fb0",
          name: "Tatiana C",
          owner: "Person14",
          reposts: [1, 12, 13, 15],
          platform: "fb",
        },
        Person15fb0: {
          id: "Person15fb0",
          name: "Raegan R",
          owner: "Person15",
          reposts: [12, 14, 16, 17],
          platform: "fb",
        },
        Person16fb0: {
          id: "Person16fb0",
          name: "Ricky G",
          owner: "Person16",
          reposts: [],
          platform: "fb",
        },
        Person17fb0: {
          id: "Person17fb0",
          name: "Maddy B",
          owner: "Person17",
          reposts: [15, 16],
          platform: "fb",
        },
        Person18fb0: {
          id: "Person18fb0",
          name: "Mike B",
          owner: "Person18",
          reposts: [2, 19],
          platform: "fb",
        },
        Person19fb0: {
          id: "Person19fb0",
          name: "Dave Van K",
          owner: "Person19",
          reposts: [18],
          platform: "fb",
        },
      },
      x: {
        Person0x0: {
          id: "Person0x0",
          name: "@Di",
          owner: "Person0",
          reposts: [0, 1, 9],
          platform: "x",
        },
        Person0x1: {
          id: "Person0x1",
          name: "@Di D",
          owner: "Person0",
          reposts: [0, 1, 9],
          platform: "x",
        },
        Person1x0: {
          id: "Person1x0",
          name: "@Ophelia",
          owner: "Person1",
          reposts: [0, 2, 12, 13, 14],
          platform: "x",
        },
        Person2x0: {
          id: "Person2x0",
          name: "@Niall",
          owner: "Person2",
          reposts: [0, 1, 2],
          platform: "x",
        },
        Person2x1: {
          id: "Person2x1",
          name: "@Niall M",
          owner: "Person2",
          reposts: [0, 1, 2],
          platform: "x",
        },
        Person6x0: {
          id: "Person6x0",
          name: "@Delia",
          owner: "Person6",
          reposts: [6, 7],
          platform: "x",
        },
        Person6x1: {
          id: "Person6x1",
          name: "@Delia F",
          owner: "Person6",
          reposts: [6, 7],
          platform: "x",
        },
        Person7x0: {
          id: "Person7x0",
          name: "@Chrissie",
          owner: "Person7",
          reposts: [6],
          platform: "x",
        },
        Person8x0: {
          id: "Person8x0",
          name: "@Coreen",
          owner: "Person8",
          reposts: [6, 7, 8],
          platform: "x",
        },
        Person8x1: {
          id: "Person8x1",
          name: "@Coreen R",
          owner: "Person8",
          reposts: [6, 7, 8],
          platform: "x",
        },
        Person9x0: {
          id: "Person9x0",
          name: "@Vito",
          owner: "Person9",
          reposts: [0, 9, 10, 11],
          platform: "x",
        },
        Person9x1: {
          id: "Person9x1",
          name: "@Vito M",
          owner: "Person9",
          reposts: [0, 9, 10, 11],
          platform: "x",
        },
        Person10x0: {
          id: "Person10x0",
          name: "@Seb",
          owner: "Person10",
          reposts: [9, 11],
          platform: "x",
        },
        Person11x0: {
          id: "Person11x0",
          name: "@Jill",
          owner: "Person11",
          reposts: [0, 9, 10],
          platform: "x",
        },
        Person12x0: {
          id: "Person12x0",
          name: "@Ashlea",
          owner: "Person12",
          reposts: [1, 13, 14],
          platform: "x",
        },
        Person13x0: {
          id: "Person13x0",
          name: "@Val",
          owner: "Person13",
          reposts: [1, 12, 14],
          platform: "x",
        },
        Person14x0: {
          id: "Person14x0",
          name: "@Tatiana",
          owner: "Person14",
          reposts: [1, 12, 13, 14],
          platform: "x",
        },
        Person14x1: {
          id: "Person14x1",
          name: "@Tatiana C",
          owner: "Person14",
          reposts: [1, 12, 13, 14],
          platform: "x",
        },
        Person16x0: {
          id: "Person16x0",
          name: "@Ricky",
          owner: "Person16",
          reposts: [],
          platform: "x",
        },
        Person17x0: {
          id: "Person17x0",
          name: "@Maddy",
          owner: "Person17",
          reposts: [15, 16],
          platform: "x",
        },
      },
    },
    posts: [
      {
        idx: 0,
        id: "xPost0",
        xPostBy: "Person13x0",
        xRepostOf: null,
      },
      {
        idx: 1,
        id: "xPost1",
        xPostBy: "Person7x0",
        xRepostOf: null,
      },
      {
        idx: 2,
        id: "xPost2",
        xPostBy: "Person14x1",
        xRepostOf: null,
      },
      {
        idx: 3,
        id: "xPost3",
        xPostBy: "Person14x0",
        xRepostOf: null,
      },
      {
        idx: 4,
        id: "xPost4",
        xPostBy: "Person12x0",
        xRepostOf: null,
      },
      {
        idx: 5,
        id: "xPost5",
        xPostBy: "Person11x0",
        xRepostOf: null,
      },
      {
        idx: 6,
        id: "xPost6",
        xPostBy: "Person9x0",
        xRepostOf: "xPost5",
      },
      {
        idx: 7,
        id: "xPost7",
        xPostBy: "Person9x1",
        xRepostOf: "xPost5",
      },
      {
        idx: 8,
        id: "xPost8",
        xPostBy: "Person10x0",
        xRepostOf: "xPost5",
      },
      {
        idx: 9,
        id: "xPost9",
        xPostBy: "Person6x0",
        xRepostOf: null,
      },
      {
        idx: 10,
        id: "xPost10",
        xPostBy: "Person17x0",
        xRepostOf: null,
      },
      {
        idx: 11,
        id: "xPost11",
        xPostBy: "Person17x0",
        xRepostOf: null,
      },
      {
        idx: 12,
        id: "xPost12",
        xPostBy: "Person6x0",
        xRepostOf: null,
      },
      {
        idx: 13,
        id: "xPost13",
        xPostBy: "Person6x1",
        xRepostOf: null,
      },
      {
        idx: 14,
        id: "xPost14",
        xPostBy: "Person14x0",
        xRepostOf: null,
      },
      {
        idx: 15,
        id: "xPost15",
        xPostBy: "Person17x0",
        xRepostOf: null,
      },
      {
        idx: 16,
        id: "xPost16",
        xPostBy: "Person14x1",
        xRepostOf: null,
      },
      {
        idx: 17,
        id: "xPost17",
        xPostBy: "Person6x1",
        xRepostOf: null,
      },
      {
        idx: 18,
        id: "xPost18",
        xPostBy: "Person17x0",
        xRepostOf: null,
      },
      {
        idx: 19,
        id: "xPost19",
        xPostBy: "Person8x0",
        xRepostOf: null,
      },
      {
        idx: 20,
        id: "xPost20",
        xPostBy: "Person11x0",
        xRepostOf: null,
      },
      {
        idx: 21,
        id: "xPost21",
        xPostBy: "Person2x1",
        xRepostOf: null,
      },
      {
        idx: 22,
        id: "xPost22",
        xPostBy: "Person1x0",
        xRepostOf: "xPost21",
      },
      {
        idx: 23,
        id: "xPost23",
        xPostBy: "Person2x0",
        xRepostOf: "xPost21",
      },
      {
        idx: 24,
        id: "xPost24",
        xPostBy: "Person8x0",
        xRepostOf: null,
      },
      {
        idx: 25,
        id: "xPost25",
        xPostBy: "Person6x1",
        xRepostOf: null,
      },
      {
        idx: 26,
        id: "xPost26",
        xPostBy: "Person6x0",
        xRepostOf: null,
      },
      {
        idx: 27,
        id: "xPost27",
        xPostBy: "Person2x0",
        xRepostOf: null,
      },
      {
        idx: 28,
        id: "xPost28",
        xPostBy: "Person7x0",
        xRepostOf: null,
      },
      {
        idx: 29,
        id: "xPost29",
        xPostBy: "Person6x1",
        xRepostOf: "xPost28",
      },
      {
        idx: 30,
        id: "xPost30",
        xPostBy: "Person8x0",
        xRepostOf: "xPost28",
      },
      {
        idx: 31,
        id: "xPost31",
        xPostBy: "Person8x1",
        xRepostOf: "xPost30",
      },
      {
        idx: 32,
        id: "xPost32",
        xPostBy: "Person8x1",
        xRepostOf: null,
      },
      {
        idx: 33,
        id: "xPost33",
        xPostBy: "Person8x0",
        xRepostOf: "xPost32",
      },
      {
        idx: 34,
        id: "xPost34",
        xPostBy: "Person8x1",
        xRepostOf: null,
      },
      {
        idx: 35,
        id: "xPost35",
        xPostBy: "Person14x1",
        xRepostOf: null,
      },
      {
        idx: 36,
        id: "xPost36",
        xPostBy: "Person14x0",
        xRepostOf: "xPost35",
      },
      {
        idx: 37,
        id: "xPost37",
        xPostBy: "Person16x0",
        xRepostOf: null,
      },
      {
        idx: 38,
        id: "xPost38",
        xPostBy: "Person17x0",
        xRepostOf: "xPost37",
      },
      {
        idx: 39,
        id: "xPost39",
        xPostBy: "Person9x1",
        xRepostOf: null,
      },
      {
        idx: 40,
        id: "xPost40",
        xPostBy: "Person10x0",
        xRepostOf: null,
      },
      {
        idx: 41,
        id: "xPost41",
        xPostBy: "Person9x0",
        xRepostOf: "xPost40",
      },
      {
        idx: 42,
        id: "xPost42",
        xPostBy: "Person9x1",
        xRepostOf: "xPost40",
      },
      {
        idx: 43,
        id: "xPost43",
        xPostBy: "Person0x1",
        xRepostOf: "xPost42",
      },
      {
        idx: 44,
        id: "xPost44",
        xPostBy: "Person11x0",
        xRepostOf: "xPost42",
      },
      {
        idx: 45,
        id: "xPost45",
        xPostBy: "Person8x1",
        xRepostOf: null,
      },
      {
        idx: 46,
        id: "xPost46",
        xPostBy: "Person6x0",
        xRepostOf: null,
      },
      {
        idx: 47,
        id: "xPost47",
        xPostBy: "Person6x1",
        xRepostOf: "xPost46",
      },
      {
        idx: 48,
        id: "xPost48",
        xPostBy: "Person7x0",
        xRepostOf: "xPost47",
      },
      {
        idx: 49,
        id: "xPost49",
        xPostBy: "Person8x0",
        xRepostOf: "xPost47",
      },

// ...truncated 1721 lines
<!doctype html>
<html lang="en" style="background-color: #2d383f">
  <head>
    <meta charset="utf-8" />
    <title>Social Media Analysis</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="@fortawesome/[email protected]/css/brands.css"
    />
  </head>
  <body>
    <div id="klchart" class="klchart"></div>
    <script type="module" src="./code.js"></script>
  </body>
</html>

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.