This demo illustrates the powerful analysis possible when the data evolves over time. KeyLines can be used to investigate the 'when' as well as the 'what'.
By displaying the graph simultaneously with the time bar a variety of interactions are possible. With the full dataset shown, selecting nodes on the graph will automatically highlight their activity on the time bar, providing insight as to the distribution of activity (phone calls in this case) over the whole time period.
Use of animation
Additional insight can be gained by playing an animation of the chart. If the time bar sliders are narrowed to approximately 1 month (September) and the play button pressed then the dynamic evolution of the data (representing a social network) can be seen.
Operation and appearance
When 'paused', the time bar can be dragged backwards and forwards to find the exact moment of an event. Note the use of KeyLines's ping function to draw attention to nodes entering the chart's selected timeframe.
Data
McDiarmid, A. (Creator), Irvine, J. (Creator), Bell, S. D. (Creator), Banford, J. (Creator). (Apr 2015). Nodobo mobile phone usage data. CRAWDAD. 10.15783/C7HP4Q
Key functions used:
- chart.filter
- chart.foreground
- chart.selection
- chart.graph().neighbours
- Chart Events
- timebar.selection
- Time bar Events
import KeyLines from "keylines";
import { chartData, baseColor, selectionColours } from "./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: "/public/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); export const chartData = {
type: "LinkChart",
items: [
{
type: "node",
c: "#c0c0c0",
id: "9",
t: "9",
},
{
type: "node",
c: "#c0c0c0",
id: "127",
t: "127",
},
{
type: "node",
c: "#c0c0c0",
id: "129",
t: "129",
},
{
type: "node",
c: "#c0c0c0",
id: "157",
t: "157",
},
{
type: "node",
c: "#c0c0c0",
id: "192",
t: "192",
},
{
type: "node",
c: "#c0c0c0",
id: "193",
t: "193",
},
{
type: "node",
c: "#c0c0c0",
id: "208",
t: "208",
},
{
type: "node",
c: "#c0c0c0",
id: "244",
t: "244",
},
{
type: "node",
c: "#c0c0c0",
id: "372",
t: "372",
},
{
type: "node",
c: "#c0c0c0",
id: "452",
t: "452",
},
{
type: "node",
c: "#c0c0c0",
id: "455",
t: "455",
},
{
type: "node",
c: "#c0c0c0",
id: "563",
t: "563",
},
{
type: "node",
c: "#c0c0c0",
id: "901",
t: "901",
},
{
type: "node",
c: "#c0c0c0",
id: "908",
t: "908",
},
{
type: "node",
c: "#c0c0c0",
id: "1641",
t: "1641",
},
{
type: "node",
c: "#c0c0c0",
id: "1740",
t: "1740",
},
{
type: "node",
c: "#c0c0c0",
id: "2172",
t: "2172",
},
{
type: "node",
c: "#c0c0c0",
id: "2327",
t: "2327",
},
{
type: "node",
c: "#c0c0c0",
id: "2330",
t: "2330",
},
{
type: "node",
c: "#c0c0c0",
id: "2367",
t: "2367",
},
{
type: "node",
c: "#c0c0c0",
id: "2432",
t: "2432",
},
{
type: "node",
c: "#c0c0c0",
id: "14099",
t: "14099",
},
{
type: "node",
c: "#c0c0c0",
id: "21261",
t: "21261",
},
{
type: "node",
c: "#c0c0c0",
id: "45347",
t: "45347",
},
{
type: "node",
c: "#c0c0c0",
id: "2284122",
t: "2284122",
},
{
type: "node",
c: "#c0c0c0",
id: "3305122",
t: "3305122",
},
{
type: "node",
c: "#c0c0c0",
id: "3360769",
t: "3360769",
},
{
type: "node",
c: "#c0c0c0",
id: "3371982",
t: "3371982",
},
{
type: "node",
c: "#c0c0c0",
id: "3396625",
t: "3396625",
},
{
type: "node",
c: "#c0c0c0",
id: "5505813",
t: "5505813",
},
{
type: "node",
c: "#c0c0c0",
id: "5844903",
t: "5844903",
},
{
type: "node",
c: "#c0c0c0",
id: "7792837",
t: "7792837",
},
{
type: "node",
c: "#c0c0c0",
id: "8489720",
t: "8489720",
},
{
type: "node",
c: "#c0c0c0",
id: "22400894",
t: "22400894",
},
{
type: "node",
c: "#c0c0c0",
id: "07434677419",
t: "07434677419",
},
{
type: "node",
c: "#c0c0c0",
id: "07610039694",
t: "07610039694",
},
{
type: "node",
c: "#c0c0c0",
id: "07588304495",
t: "07588304495",
},
{
type: "node",
c: "#c0c0c0",
id: "07641036117",
t: "07641036117",
},
{
type: "node",
c: "#c0c0c0",
id: "07981267897",
t: "07981267897",
},
{
type: "node",
c: "#c0c0c0",
id: "07784425582",
t: "07784425582",
},
{
type: "node",
c: "#c0c0c0",
id: "07743039441",
t: "07743039441",
},
{
type: "node",
c: "#c0c0c0",
id: "07163185791",
t: "07163185791",
},
{
type: "node",
c: "#c0c0c0",
id: "01850897526",
t: "01850897526",
},
{
type: "node",
c: "#c0c0c0",
id: "07066875066",
t: "07066875066",
},
{
type: "node",
c: "#c0c0c0",
id: "07691640598",
t: "07691640598",
},
{
type: "node",
c: "#c0c0c0",
id: "07187432175",
t: "07187432175",
},
{
type: "node",
c: "#c0c0c0",
id: "07230262224",
t: "07230262224",
},
{
type: "node",
c: "#c0c0c0",
id: "07110730864",
t: "07110730864",
},
{
type: "node",
c: "#c0c0c0",
id: "07209670163",
t: "07209670163",
},
{
type: "node",
c: "#c0c0c0",
id: "48772130600436761996023412922286",
t: "48772130600436761996023412922286",
},
{
type: "node",
c: "#c0c0c0",
id: "07501874156",
t: "07501874156",
},
{
type: "node",
c: "#c0c0c0",
id: "07681546436",
t: "07681546436",
},
{
type: "node",
c: "#c0c0c0",
id: "07914559382",
t: "07914559382",
},
{
type: "node",
c: "#c0c0c0",
id: "07304637408",
t: "07304637408",
},
{
type: "node",
c: "#c0c0c0",
id: "07087748678",
t: "07087748678",
},
{
type: "node",
c: "#c0c0c0",
id: "07211022593",
t: "07211022593",
},
{
type: "node",
c: "#c0c0c0",
id: "01256721926",
t: "01256721926",
},
{
type: "node",
c: "#c0c0c0",
id: "07607124303",
t: "07607124303",
},
{
type: "node",
c: "#c0c0c0",
id: "07029691700",
t: "07029691700",
},
{
type: "node",
c: "#c0c0c0",
id: "07839321359",
t: "07839321359",
},
{
type: "node",
c: "#c0c0c0",
id: "07651268820",
t: "07651268820",
},
{
type: "node",
c: "#c0c0c0",
id: "01499302899",
t: "01499302899",
},
{
type: "node",
c: "#c0c0c0",
id: "07854607195",
t: "07854607195",
},
{
type: "node",
c: "#c0c0c0",
id: "07080329376",
t: "07080329376",
},
{
type: "node",
c: "#c0c0c0",
id: "07167454533",
t: "07167454533",
},
{
type: "node",
c: "#c0c0c0",
id: "07162108686",
t: "07162108686",
},
{
type: "node",
c: "#c0c0c0",
id: "07541486388",
t: "07541486388",
},
{
type: "node",
c: "#c0c0c0",
id: "07195393761",
t: "07195393761",
},
{
type: "node",
c: "#c0c0c0",
id: "01780890590",
t: "01780890590",
},
{
type: "node",
c: "#c0c0c0",
id: "07978424233",
t: "07978424233",
},
{
type: "node",
c: "#c0c0c0",
id: "07946912731",
t: "07946912731",
},
{
type: "node",
c: "#c0c0c0",
id: "07225912655",
t: "07225912655",
},
{
type: "node",
c: "#c0c0c0",
id: "07400578851",
t: "07400578851",
},
{
type: "node",
c: "#c0c0c0",
id: "01546219380",
t: "01546219380",
},
{
type: "node",
c: "#c0c0c0",
id: "07150470306",
t: "07150470306",
},
{
type: "node",
c: "#c0c0c0",
id: "07266516764",
t: "07266516764",
},
{
type: "node",
c: "#c0c0c0",
id: "07652081795",
t: "07652081795",
},
{
type: "node",
c: "#c0c0c0",
id: "07817341678",
t: "07817341678",
},
{
type: "node",
c: "#c0c0c0",
id: "07869748073",
t: "07869748073",
},
{
type: "node",
c: "#c0c0c0",
id: "07279151148",
t: "07279151148",
},
{
type: "node",
c: "#c0c0c0",
id: "07613917675",
t: "07613917675",
},
{
type: "node",
c: "#c0c0c0",
id: "07771486427",
t: "07771486427",
},
{
type: "node",
c: "#c0c0c0",
id: "07587572122",
t: "07587572122",
},
{
type: "node",
c: "#c0c0c0",
id: "6061629169",
t: "6061629169",
},
{
type: "node",
c: "#c0c0c0",
id: "07758045718",
t: "07758045718",
},
{
type: "node",
c: "#c0c0c0",
id: "07394508492",
t: "07394508492",
},
{
type: "node",
c: "#c0c0c0",
id: "01887771702",
t: "01887771702",
},
{
type: "node",
c: "#c0c0c0",
id: "07806391587",
t: "07806391587",
},
{
type: "node",
c: "#c0c0c0",
id: "07909763438",
t: "07909763438",
},
{
type: "node",
c: "#c0c0c0",
id: "07028004429",
t: "07028004429",
},
{
type: "node",
c: "#c0c0c0",
id: "07392737002",
t: "07392737002",
},
{
type: "node",
c: "#c0c0c0",
id: "07942503106",
t: "07942503106",
},
{
type: "node",
c: "#c0c0c0",
id: "07090017672",
t: "07090017672",
},
{
type: "node",
c: "#c0c0c0",
id: "07962175133",
t: "07962175133",
},
{
type: "node",
c: "#c0c0c0",
id: "07651896055",
t: "07651896055",
},
{
type: "node",
c: "#c0c0c0",
id: "01022354521",
t: "01022354521",
},
{
type: "node",
c: "#c0c0c0",
id: "07830667924",
t: "07830667924",
},
{
type: "node",
c: "#c0c0c0",
id: "07574455931",
t: "07574455931",
},
{
type: "node",
c: "#c0c0c0",
id: "01954651441",
t: "01954651441",
},
{
type: "node",
c: "#c0c0c0",
id: "07122915122",
t: "07122915122",
},
{
type: "node",
c: "#c0c0c0",
id: "01815280008",
t: "01815280008",
},
{
type: "node",
c: "#c0c0c0",
id: "07682111000",
t: "07682111000",
},
{
type: "node",
c: "#c0c0c0",
id: "07969251842",
t: "07969251842",
},
{
type: "node",
c: "#c0c0c0",
id: "07221387474",
t: "07221387474",
},
{
type: "node",
c: "#c0c0c0",
id: "07375968743",
t: "07375968743",
},
{
type: "node",
c: "#c0c0c0",
id: "044",
t: "044",
},
{
type: "node",
c: "#c0c0c0",
id: "07151178213",
t: "07151178213",
},
{
type: "node",
c: "#c0c0c0",
id: "07197268544",
t: "07197268544",
},
{
type: "node",
c: "#c0c0c0",
id: "07014881393",
t: "07014881393",
},
{
type: "node",
c: "#c0c0c0",
id: "07246826358",
t: "07246826358",
},
{
type: "node",
c: "#c0c0c0",
id: "07548213987",
t: "07548213987",
},
{
type: "node",
c: "#c0c0c0",
id: "14191086749599",
t: "14191086749599",
},
{
type: "node",
c: "#c0c0c0",
id: "14853692371599",
t: "14853692371599",
},
{
type: "node",
c: "#c0c0c0",
id: "07896182404",
t: "07896182404",
},
{
type: "node",
c: "#c0c0c0",
id: "07118310788",
t: "07118310788",
},
{
type: "node",
c: "#c0c0c0",
id: "07944968233",
t: "07944968233",
},
{
type: "node",
c: "#c0c0c0",
id: "07720520621",
t: "07720520621",
},
{
type: "node",
c: "#c0c0c0",
id: "07775682498",
t: "07775682498",
},
{
type: "node",
c: "#c0c0c0",
id: "07023552017",
t: "07023552017",
},
{
type: "node",
c: "#c0c0c0",
id: "07340142898",
t: "07340142898",
},
{
type: "node",
c: "#c0c0c0",
id: "07634569803",
t: "07634569803",
},
{
type: "node",
c: "#c0c0c0",
id: "01575974486",
t: "01575974486",
},
{
type: "node",
c: "#c0c0c0",
id: "07153234302",
t: "07153234302",
},
{
type: "node",
c: "#c0c0c0",
id: "07633839447",
t: "07633839447",
},
{
type: "node",
c: "#c0c0c0",
id: "07341891255",
t: "07341891255",
},
{
type: "node",
c: "#c0c0c0",
id: "07926168565",
t: "07926168565",
},
{
type: "node",
c: "#c0c0c0",
id: "07873904393",
t: "07873904393",
},
{
type: "node",
c: "#c0c0c0",
id: "07976349200",
t: "07976349200",
},
{
type: "node",
c: "#c0c0c0",
id: "07476886744",
t: "07476886744",
},
{
type: "node",
c: "#c0c0c0",
id: "01527646556",
t: "01527646556",
},
{
type: "node",
c: "#c0c0c0",
id: "07318250386",
t: "07318250386",
},
{
type: "node",
c: "#c0c0c0",
id: "07806980885",
t: "07806980885",
},
{
type: "node",
c: "#c0c0c0",
id: "07514503190",
t: "07514503190",
},
{
type: "node",
c: "#c0c0c0",
id: "07456622368",
t: "07456622368",
},
{
type: "node",
c: "#c0c0c0",
id: "07245763413",
t: "07245763413",
},
{
type: "node",
c: "#c0c0c0",
id: "07676963558",
t: "07676963558",
},
{
type: "node",
c: "#c0c0c0",
id: "07613569456",
t: "07613569456",
},
{
type: "node",
c: "#c0c0c0",
id: "07776253730",
t: "07776253730",
},
{
type: "node",
c: "#c0c0c0",
id: "07102745960",
t: "07102745960",
},
{
type: "node",
c: "#c0c0c0",
id: "07389444649",
t: "07389444649",
},
{
type: "node",
c: "#c0c0c0",
id: "07126482937",
t: "07126482937",
},
{
type: "node",
c: "#c0c0c0",
id: "07966846548",
t: "07966846548",
},
{
type: "node",
c: "#c0c0c0",
id: "07833688765",
t: "07833688765",
},
{
type: "node",
c: "#c0c0c0",
id: "01739521988",
t: "01739521988",
},
{
type: "node",
c: "#c0c0c0",
id: "07585377112",
t: "07585377112",
},
{
type: "node",
c: "#c0c0c0",
id: "07294484859",
t: "07294484859",
},
{
type: "node",
c: "#c0c0c0",
id: "07850316631",
t: "07850316631",
},
{
type: "node",
c: "#c0c0c0",
id: "07520192003",
t: "07520192003",
},
{
type: "node",
c: "#c0c0c0",
id: "07877889057",
t: "07877889057",
},
{
type: "node",
c: "#c0c0c0",
id: "07073084322",
t: "07073084322",
},
{
type: "node",
c: "#c0c0c0",
id: "07492710736",
t: "07492710736",
},
{
type: "node",
c: "#c0c0c0",
id: "07070949771",
t: "07070949771",
},
{
type: "node",
c: "#c0c0c0",
id: "07252637450",
t: "07252637450",
},
{
type: "node",
c: "#c0c0c0",
id: "07979079238",
t: "07979079238",
},
{
type: "node",
c: "#c0c0c0",
id: "01770637968",
t: "01770637968",
},
{
type: "node",
c: "#c0c0c0",
id: "0126233087",
t: "0126233087",
},
{
type: "node",
c: "#c0c0c0",
id: "01237303110",
t: "01237303110",
},
{
type: "node",
c: "#c0c0c0",
id: "14338475884741",
t: "14338475884741",
},
{
type: "node",
c: "#c0c0c0",
id: "07873361203",
t: "07873361203",
},
{
type: "node",
c: "#c0c0c0",
id: "07189233361",
t: "07189233361",
},
{
type: "node",
c: "#c0c0c0",
id: "07497354521",
t: "07497354521",
},
{
type: "node",
c: "#c0c0c0",
id: "07541477895",
t: "07541477895",
},
{
type: "node",
c: "#c0c0c0",
id: "07690700998",
t: "07690700998",
},
{
type: "node",
c: "#c0c0c0",
id: "07351039125",
t: "07351039125",
},
{
type: "node",
c: "#c0c0c0",
id: "07529309344",
t: "07529309344",
},
{
type: "node",
c: "#c0c0c0",
id: "07216812571",
t: "07216812571",
},
{
// ...truncated 16601 lines <!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="@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" />
<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>
</head>
<body>
<div id="klchart" class="klchart klchart-timebar"></div>
<div id="kltimebar" class="kltimebar"></div>
<script type="module" src="./code.js"></script>
</body>
</html>