//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//
//     Sample Code
//
//!    Select, compare and display time-based activities.
import KeyLines from "keylines";
import { chartData, baseColor, selectionColours } from "./selections-data.js";

let chart;
let timebar;

const hideElement = document.getElementById("hide");
const selectionOnlyElement = document.getElementById("selonly");
const tooltipElement = document.getElementById("tooltip");
const chartElement = document.getElementById("klchart");
const arrow = document.querySelector(".tooltip-arrow");
const inner = document.querySelector(".tooltip-inner");

// Find the neighbours of the selection to two levels deep
function computeNeighboursOfSelection() {
  let items = chart.selection();

  if (items.length > 0) {
    const neighbours = chart.graph().neighbours(items, { hops: 2 });
    items = items.concat(neighbours.nodes, neighbours.links);
    return new Set(items);
  }
  return new Set();
}

function showSelection() {
  const visualProperties = {};

  chart.each({ type: "all" }, (item) => {
    visualProperties[item.id] = { id: item.id, c: baseColor, ha1: false };
  });

  let selectedIds = chart.selection();

  // we only colour the first few items of the selection
  selectedIds = selectedIds.slice(0, selectionColours.length);

  // clear all selections
  timebar.selection([]);

  const items = [];

  selectedIds.forEach((id, index) => {
    const fromNodes = [];
    let links = [];
    let toNodes = [];

    const item = chart.getItem(id);

    if (item.type === "node") {
      fromNodes.push(id);

      // 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 toNeighbours = chart
        .graph()
        .neighbours(id, { direction: "from", all: true });
      links = toNeighbours.links;
      toNodes = toNeighbours.nodes;
    } else {
      // it is a link
      links.push(id);

      // use the arrows to decide which list to put the nodes on
      (item.a1 ? toNodes : fromNodes).push(item.id1);
      (item.a2 ? toNodes : fromNodes).push(item.id2);
    }

    fromNodes.concat(links).forEach((itemId) => {
      visualProperties[itemId].c = selectionColours[index];
    });

    toNodes.forEach((nodeId) => {
      visualProperties[nodeId].ha1 = {
        w: 5,
        r: 27,
        c: selectionColours[index],
      };
    });

    // and select in the timebar
    // be careful to include the id of the thing selected too - this is necessary when selecting
    // links only
    items.push({ id: links, index, c: selectionColours[index] });
  });

  timebar.selection(items);

  chart.setProperties(Object.values(visualProperties));
}

async function afterFiltering(nodesShown, nodesHidden, selBefore) {
  const connectedItems = selectionOnlyElement.checked
    ? computeNeighboursOfSelection()
    : new Set();

  let pingNodes = nodesShown;
  if (connectedItems.size) {
    const result = await chart.filter((item) => connectedItems.has(item.id), {
      animate: false,
    });
    pingNodes = pingNodes.concat(result.shown.nodes);
  }

  // if the nodes displayed have changed then run an adaptive layout
  if (pingNodes.length > 0 || nodesHidden.length > 0) {
    chart.layout("organic", { animate: true, time: 500, mode: "adaptive" });
  }

  if (pingNodes.length > 0) {
    chart.ping(pingNodes, { time: 1500, c: "rgb(178, 38, 9)" });
  }

  // If our filtering has hidden any selected items, then we need to update
  // the selection lines in the timebar
  if (selBefore.length !== chart.selection().length) {
    showSelection();
  }
}

async function timebarChanged() {
  if (hideElement.checked) {
    const selBefore = chart.selection();
    const result = await chart.filter(timebar.inRange, {
      animate: false,
      type: "link",
    });
    afterFiltering(result.shown.nodes, result.hidden.nodes, selBefore);
  } else {
    chart.foreground(timebar.inRange, { type: "link" });
  }
}

// When the user changes the hide checkbox, swap items from
// being hidden to background items
function hideToggled() {
  const hi = hideElement.checked;

  const items = [];
  chart.each({ type: "all" }, (item) => {
    if (item[hi ? "bg" : "hi"]) {
      items.push({ id: item.id, bg: !hi, hi });
    }
  });
  chart.setProperties(items);
}

function showTooltip({ type, index, value, tooltipX, tooltipY }) {
  // the offset is taken from the top-left corner of the chart
  // calculate the height of the chart to start from the top-left of the timebar
  const offset = { top: document.getElementById("kl").height, left: 0 };

  const toShow = type === "bar" || type === "selection";

  if (toShow) {
    // change the content
    document.getElementById("tooltipText").innerText = `Value: ${value}`;

    // selection colour or default colour for bar hover
    const tooltipColour = selectionColours[index] || "";

    // style both the tooltip body and the arrow
    arrow.style.backgroundColor = tooltipColour;
    inner.style.backgroundColor = tooltipColour;

    // in case of default colour or red (selection 2)
    const isBetterWhite = tooltipColour.length === 0 || index > 1;

    // white works better in same cases
    inner.style.color = isBetterWhite ? "white" : "black";

    // the top needs to be adjusted to accommodate the height of the tooltip,
    // the chart height and an aesthetic offset
    offset.top =
      tooltipY - (tooltipElement.clientHeight + 8) + chartElement.clientHeight;

    // shift left by half width to centre it
    offset.left = tooltipX - tooltipElement.clientWidth / 2 + 8;
  }

  // set the position and toggle the "in" class to show/hide the tooltip
  tooltipElement.style.left = `${offset.left}px`;
  tooltipElement.style.top = `${offset.top}px`;
  if (toShow) {
    tooltipElement.classList.remove("hidden");
  } else {
    tooltipElement.classList.add("hidden");
  }
}

function initaliseEvents() {
  // Chart events to react to
  chart.on("selection-change", timebarChanged);
  chart.on("selection-change", showSelection);

  // Time bar events to react to
  timebar.on("change", () => {
    tooltipElement.classList.add("hidden");
    timebarChanged();
  });
  timebar.on("hover", showTooltip);

  hideElement.addEventListener("click", () => {
    hideToggled();
    const checked = hideElement.checked;
    selectionOnlyElement.disabled = !checked;
    if (selectionOnlyElement.disabled) {
      selectionOnlyElement.checked = false;
    }
    document.getElementById("solab").style.color = checked ? "" : "silver";
  });
  selectionOnlyElement.addEventListener("click", timebarChanged);
}

async function startKeyLines() {
  const chartOptions = {
    handMode: true,
    logo: { u: "/images/Logo.png" },
    overview: { icon: false, shown: false },
  };

  const timeBarOptions = {
    playSpeed: 30,
  };

  [chart, timebar] = await KeyLines.create([
    { id: "kl", container: "klchart", type: "chart", options: chartOptions },
    { container: "kltimebar", type: "timebar", options: timeBarOptions },
  ]);

  initaliseEvents();

  // load the data into timebar and chart
  timebar.load(chartData);
  chart.load(chartData);
  timebar.zoom("fit", { animate: false });
  chart.layout("organic", { packing: "circle" });
}

window.addEventListener("DOMContentLoaded", startKeyLines);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
  <head>
    <meta charset="UTF-8">
    <title>Animate Filtering by Time</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">
    <style type="text/css">
      .tooltip {
        display: block;
        opacity: 0.5;
        position: absolute;
        pointer-events: none;
        z-index: 1;
      }
      .tooltip-inner {
        line-height: 30px;
        margin: 0px;
      }
      .tooltip-inner p {
        margin: 0px;
      }
      .tooltip.top .tooltip-arrow {
        display: unset;
        width: 20px;
        height: 20px;
        background-color: #2d3741;
        transform: rotateZ(45deg);
        position: absolute;
        top: 29px;
        left: 29px;
      }
      
    </style>
    <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="/selections.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>
          <div class="kltimebar" id="kltimebar">
          </div>
          <div class="tooltip top hidden" id="tooltip">
            <div class="tooltip-arrow"></div>
            <div class="tooltip-inner">
              <p id="tooltipText"></p>
            </div>
          </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%;">Animate Filtering by Time</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">
                <fieldset>
                  <p>
                    Click on nodes or links in the chart to see their activity over time in the time bar. Shift-click to select multiple
                    items in the chart and compare their time profiles - the time bar displays up to three different selections.
                  </p>
                </fieldset>
                <fieldset> 
                  <legend>Options</legend>
                  <p>
                    The first check-box below controls whether items outside the time bar's time range are hidden or backgrounded. When the
                    second checkbox is checked, only items within two steps of a selected item are shown in the chart. This allows you
                    to focus on activity in the immediate neighbourhood of an item of interest.
                  </p>
                  <label class="checkbox">
                    <input id="hide" type="checkbox" checked="checked">Hide items outside the current time range
                  </label>
                  <label class="checkbox">
                    <input id="selonly" type="checkbox" checked="checked">
                    <p id="solab" style="display: inline-block;">and show only neighbours of selection</p>
                  </label>
                </fieldset>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
    <div id="moreParent">
      <div id="moreContainer">
      </div>
    </div>
    <div id="lazyScripts">
    </div>
  </body>
</html>
Loading source