//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//
//     Sample Code
//
//!    Show events that represent periods of time.
import KeyLines from "keylines";
import data from "./tbperiods-data.js";

let chart;
let timebar;
const selectionColours = [
  ["rgb(252, 0, 0)", "rgb(253, 141, 60)"],
  ["rgb(0, 0, 255)", "rgb(0, 191, 255)"],
  ["rgb(0, 128, 0)", "rgb(50,205,50)"],
];
const greyColours = ["rgb(105, 105, 105)", "rgb(192, 192, 192)"];
const tooltip = document.getElementById("tooltip");
const tooltipStart = document.getElementById("tooltip-start");
const tooltipEnd = document.getElementById("tooltip-end");

// Runs an organic layout
async function doLayout(mode = "adaptive") {
  await chart.layout("organic", { time: 400, easing: "linear", mode });
}

function getDate(timestamp) {
  if (!timestamp) {
    return "n/a";
  }
  const date = new Date(timestamp);
  return date.toDateString().slice(4);
}

function closeTooltip() {
  tooltip.classList.add("hidden");
}

function showTooltip({ id, x, y, type, pointerType }) {
  // only handle hover and touch + pen
  if (type === "pointer-down" && pointerType === "mouse") return;
  // null for the background
  const item = chart.getItem(id);
  // hovering over a link
  if (item && item.type === "link") {
    // fill-in tooltip
    tooltipStart.innerHTML = getDate(item.dt[0].dt1);
    tooltipEnd.innerHTML = getDate(item.dt[0].dt2);

    // position it relative to the event
    if (chart.viewOptions().width > x + 250) {
      tooltip.style.top = `${y - 28}px`;
      tooltip.style.left = `${x + 20}px`;
      tooltip.classList.remove("right");
    } else {
      tooltip.style.top = `${y - 28}px`;
      tooltip.style.left = `${x - 230}px`;
      tooltip.classList.add("right");
    }
    // show the tooltip
    tooltip.classList.remove("hidden");
  } else {
    // if not selecting a link, then close tooltip
    closeTooltip();
  }
}

function buildItemStyling(item, width, colour, border = false) {
  if (item.type === "node") {
    return {
      id: item.id,
      fi: {
        c: colour,
        t: item.d.type === "person" ? "fas fa-user" : "fas fa-building",
      },
      b: border ? colour : null,
    };
  }
  // else it is a link
  return { id: item.id, w: width || 3, c: colour };
}

function showSelection() {
  // clear all selections
  timebar.selection([]);
  const timebarSelection = [];

  const chartProperties = {};
  // set default colouring
  chart.each({ type: "all" }, (item) => {
    chartProperties[item.id] = buildItemStyling(
      item,
      3,
      item.type === "node" ? greyColours[0] : greyColours[1]
    );
  });

  // we only colour the first few items of the selection
  let selectedIds = chart.selection();

  if (selectedIds.length > selectionColours.length) {
    selectedIds = selectedIds.slice(0, selectionColours.length);
  }
  chart.selection(selectedIds);

  selectedIds.forEach((id, index) => {
    let neighbouringNodes = [];
    let links = [];
    const item = chart.getItem(id);
    const colour =
      index < selectionColours.length
        ? selectionColours[index][0]
        : greyColours[1];

    if (item.type === "node") {
      // note we use all: true so that we apply styles also to
      // hidden nodes and links that aren't in the current time range
      const neighbours = chart.graph().neighbours(id, { all: true });
      links = neighbours.links;
      neighbouringNodes = neighbours.nodes;

      // colour node
      chartProperties[id] = buildItemStyling(item, 3, colour, true);
    } else {
      // it is a link
      links.push(id);
      neighbouringNodes.push(item.id1, item.id2);

      // colour link
      chartProperties[id] = buildItemStyling(item, 5, colour);
    }

    // colour neighbouring nodes
    neighbouringNodes.forEach((neighbourId) => {
      if (
        chartProperties[neighbourId].fi.c === greyColours[0] &&
        index < selectionColours.length
      ) {
        chartProperties[neighbourId] = buildItemStyling(
          chart.getItem(neighbourId),
          3,
          selectionColours[index][1]
        );
      }
    });

    // and select in the timebar
    timebarSelection.push({
      id: links,
      index,
      c: colour,
    });
  });

  timebar.selection(timebarSelection);
  chart.setProperties(
    Object.keys(chartProperties).map((key) => chartProperties[key])
  );
}

function resizePeopleByDegree() {
  function normalize(max, min, value) {
    return max === min ? min : (value - min) / (max - min);
  }

  const values = chart.graph().degrees();
  const ids = Object.keys(values);
  const valuesArray = Object.keys(values).map((key) => values[key]);
  const max = Math.max.apply(null, valuesArray);
  const min = Math.min.apply(null, valuesArray);

  const resizedNodes = ids.map((id) => {
    const item = chart.getItem(id);
    const resizeValue =
      item.d.type === "person" ? normalize(max, min, values[id]) : 0;
    return { id, e: 2 * resizeValue + 1 };
  });

  chart.setProperties(resizedNodes);
}

function initialiseInteractions() {
  chart.on("hover", showTooltip);
  chart.on("pointer-down", showTooltip);
  chart.on("selection-change", showSelection);

  const layoutButton = document.getElementById("layout");
  layoutButton.addEventListener("click", () => {
    doLayout("full");
  });

  timebar.on("change", async () => {
    // filter the chart to show only items in the new range
    await chart.filter(timebar.inRange, { animate: false, type: "link" });

    // and then adjust the chart's layout
    resizePeopleByDegree();
    doLayout();
  });
}

async function loadData() {
  data.items.forEach((element) => {
    if (element.type === "node") {
      element.fi = {
        c: greyColours[0],
        t: element.d.type === "person" ? "fas fa-user" : "fas fa-building",
      };
    }
  });

  // load the data (defined in separate js file) into both the chart and time bar
  timebar.load(data);
  timebar.zoom("fit", { animate: false });

  chart.load(data);
  resizePeopleByDegree();
  await doLayout();

  initialiseInteractions();
}

async function loadKeyLines() {
  const options = {
    selectedNode: {},
    selectedLink: {},
    handMode: true,
    logo: "/images/Logo.png",
    overview: { icon: false },
    hover: 5,
    iconFontFamily: "Font Awesome 5 Free",
  };

  const tbOptions = { minScale: { units: "day" } };

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

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

  loadData();
}

function loadFonts() {
  document.fonts.load('24px "Font Awesome 5 Free"').then(loadKeyLines);
}

window.addEventListener("DOMContentLoaded", loadFonts);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
  <head>
    <meta charset="UTF-8">
    <title>Time Periods</title>
    <link rel="stylesheet" type="text/css" href="/css/keylines.css">
    <link rel="stylesheet" type="text/css" href="/css/minimalsdk.css">
    <link rel="stylesheet" type="text/css" href="/css/sdk-layout.css">
    <link rel="stylesheet" type="text/css" href="/css/demo.css">
    <link rel="stylesheet" type="text/css" href="/css/fontawesome5/fontawesome.css">
    <link rel="stylesheet" type="text/css" href="/css/fontawesome5/solid.css">
    <link rel="stylesheet" type="text/css" href="/tbperiods.css">
    <script src="/vendor/jquery.js" defer type="text/javascript"></script>
    <script id="keylines" src="/public/keylines.umd.cjs" defer type="text/javascript"></script>
    <script src="/tbperiods.js" crossorigin="use-credentials" type="module"></script>
  </head>
  <body>
    <div class="chart-wrapper demo-cols">
      <div class="tab-content-panel flex1" id="lhs" data-tab-group="rhs">
        <div class="toggle-content is-visible" id="lhsVisual" style="width:100%; height: 100%;">
          <div class="klchart klchart-timebar" id="klchart">
            <div class="hidden" id="tooltip">
              <table class="table-condensed">
                <tbody>
                  <tr>
                    <td style="text-align: right;"><strong>Appointed on:</strong></td>
                    <td id="tooltip-start">{{start}}</td>
                  </tr>
                  <tr>
                    <td style="text-align: right;"><strong>Resigned on:</strong></td>
                    <td id="tooltip-end">{{end}}</td>
                  </tr>
                </tbody>
              </table>
              <div class="arrow"></div>
            </div>
          </div>
          <div class="kltimebar" id="kltimebar">
          </div>
        </div>
      </div>
      <div class="rhs citext closed" id="demorhscontent">
        <div class="title-bar">
          <svg viewBox="0 0 360 60" style="max-width: 360px; max-height: 60px; font-size: 24px; font-family: Raleway; flex: 1;">
            <text x="0" y="38" style="width: 100%;">Time Periods</text>
          </svg>
        </div>
        <div class="tab-content-panel" data-tab-group="rhs">
          <div class="toggle-content is-visible tabcontent" id="controlsTab">
            <form autocomplete="off" onsubmit="return false" id="rhsForm">
              <div class="cicontent">
                <ul>
                  <li>Use the play control to show how relationships change over time.</li>
                  <li>Double-click/double-tap a month or year to filter the chart.</li>
                  <li>Hover over/tap a link to show the start/end dates of an entrepreneur's directorship.</li>
                  <li>Multi-select up to three nodes or links to highlight neighbours.</li>
                </ul>
                <div style="margin-top:30px;">
                  <input class="btn btn-block btn-info" type="button" value="Layout" id="layout">
                </div>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
    <div id="moreParent">
      <div id="moreContainer">
      </div>
    </div>
    <div id="lazyScripts">
    </div>
  </body>
</html>
#tooltip {
  position: absolute;
  float: left;
  border: 1px solid #ccc;
  background-color: #fff;
  z-index: 1;
  width: 208px;
}

#layout {
  text-align: center;
  margin-top:18px;
}

.arrow {
  position: absolute;
  top: 20px;
  left: -9px;
  border-width: 0px 0px 1px 1px;
  border-style: solid;
  border-color: #ccc;
  background-color: #fff;
  transform: rotateZ(45deg);
  height: 15px;
  width: 15px;
}

.right .arrow {
  left: 199px;
  transform: rotateZ(225deg);
}
Loading source