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