//
// 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
