//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
// Sample Code
// Use React to create time bar and chart with font icons and tooltips.
//
// demo uses webpack to manage dependencies and build
import React, { useState, useRef, useEffect } from "react";
import { render } from "react-dom";
import { Chart, Timebar } from "react-keylines";
import { LeftPanel, RightPanel } from "react-demo";
import KeyLines from "keylines";
const Tooltip = ({ tooltipState }) => {
return (
<span
id="tooltip"
className={tooltipState.tooltipClass}
style={{ left: tooltipState.left, top: tooltipState.top }}
>
<div className="arrow"></div>
<table>
<tbody>
<tr>
<td className="table-cell">
<b>Appointed on: </b>
</td>
<td>{tooltipState.start}</td>
</tr>
<tr>
<td className="table-cell">
<b>Resigned on: </b>
</td>
<td>{tooltipState.end}</td>
</tr>
</tbody>
</table>
</span>
);
};
const ReactDemo = () => {
// data is a global variable stored in reactchartfilter-data.js
const [state, setState] = useState(data);
const [handMode, setHandMode] = useState(true);
const [timebarSelection, setTimebarSelection] = useState([]);
const [tooltipState, setTooltipState] = useState({
start: "",
end: "",
left: "",
top: "",
tooltipClass: "hidden",
});
// here we save the reference to the chart and time bar so we can call KeyLines' methods on them
const chart = useRef(null);
const timebar = useRef(null);
const greyColours = ["rgb(105, 105, 105)", "rgb(192, 192, 192)"];
// only styling the first 3 selected items as the timebar can only show three selection lines
const selectionColours = [
["#9600d5", "#d0a3cb"],
["#0044ff", "#6b93ff"],
["#ff0000", "#ff9e9e"],
];
const updateHandMode = ({ id, preventDefault }) => {
if (id === "_toggleMode") {
preventDefault();
setHandMode(!handMode);
}
};
const toggleTooltip = ({ id, x, y }) => {
const getDateFromTimestamp = (timestamp) => {
return timestamp ? new Date(timestamp).toDateString().slice(4) : "n/a";
};
if (chart.current) {
const item = id ? chart.current.getItem(id) : null;
const viewWidth = chart.current.viewOptions().width;
let newTooltipState = {};
if (item && item.type === "link") {
const timebarItem = data.items.filter((tbItem) => {
if (tbItem.id === item.id) {
return tbItem;
}
});
newTooltipState.start = getDateFromTimestamp(timebarItem[0].dt[0].dt1);
newTooltipState.end = getDateFromTimestamp(timebarItem[0].dt[0].dt2);
// style tooltip to left of cursor if it's x position + width exceeds
// the remaining chart space.
if (viewWidth && viewWidth > x + 250) {
newTooltipState.top = `${y - 24}px`;
newTooltipState.left = `${x + 22}px`;
newTooltipState.tooltipClass = "";
} else {
newTooltipState.top = `${y - 24}px`;
newTooltipState.left = `${x - 218}px`;
newTooltipState.tooltipClass = "right";
}
} else {
newTooltipState.tooltipClass = "hidden";
}
setTooltipState(newTooltipState);
}
};
// filter the items on the chart according to the range of the timebar
const onTimebarChange = () => {
// if all our components are loaded
if (chart.current && timebar.current) {
// filter the chart to show only items in the new range
chart.current
.filter(timebar.current.inRange, { animate: false, type: "link" })
.then(() => {
// and then adjust the chart's layout
chart.current.layout("organic", {
time: 500,
mode: "adaptive",
easing: "linear",
});
});
}
};
const 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's a link
return { id: item.id, w: width || 4, c: colour };
};
const highlightItemsOnSelection = () => {
const timebarSelection = [];
const chartProperties = {};
// set default colouring
chart.current.each({ type: "all" }, (item) => {
chartProperties[item.id] = buildItemStyling(
item,
3,
item.type === "node" ? greyColours[0] : greyColours[1]
);
});
let selectedIds = chart.current.selection();
if (selectedIds) {
// prevent the selection of more than 3 items as the timebar can only show three selection lines
if (selectedIds.length > selectionColours.length) {
selectedIds = selectedIds.slice(0, selectionColours.length);
chart.current.selection(selectedIds);
}
selectedIds.forEach((id, index) => {
let neighbouringNodes = [];
let links = [];
const item = chart.current?.getItem(id);
const colour =
index < selectionColours.length
? selectionColours[index][0]
: greyColours[1];
const linkColour = selectionColours[index][1];
if (item && item.type === "node") {
// all: true - applies styles to hidden nodes + links not in current time range
const neighbours = chart.current
?.graph()
.neighbours(id, { all: true });
if (neighbours && neighbours.links) {
links = neighbours.links;
neighbouringNodes = neighbours.nodes;
}
// colour node
chartProperties[id] = buildItemStyling(item, 3, colour, true);
links.forEach((link) => {
const linkItem = chart.current?.getItem(link);
chartProperties[link] = buildItemStyling(linkItem, 5, linkColour);
});
} else if (item && item.type === "link") {
links.push(id);
neighbouringNodes.push(item.id1, item.id2);
// colour link
chartProperties[id] = buildItemStyling(item, 5, colour);
}
// colour neighbour nodes
neighbouringNodes.forEach((neighbourId) => {
if (
chartProperties[neighbourId].fi.c === greyColours[0] &&
index < selectionColours.length
) {
chartProperties[neighbourId] = buildItemStyling(
chart.current?.getItem(neighbourId),
3,
selectionColours[index][1]
);
}
});
// and select in the timebar
timebarSelection.push({
id: links,
index,
c: colour,
});
});
setTimebarSelection(timebarSelection);
chart.current.setProperties(
Object.keys(chartProperties).map((key) => chartProperties[key])
);
}
};
// get a reference to the loaded timebar
const loadedTimebar = (newTimebar) => {
timebar.current = newTimebar;
timebar.current.mark([
{
dt1: new Date("01 Jan 2018 01:00:00"),
dt2: new Date("01 Jan 2050 01:00:00"),
},
]);
};
// get a reference to the loaded chart
const loadedChart = (newChart) => {
chart.current = newChart;
};
const loadFontIcons = () => {
let newItems = [];
state.items.forEach((item) => {
if (item.type === "node") {
newItems = [
...newItems,
{
...item,
fi: {
c: greyColours[0],
t: item.d.type === "person" ? "fas fa-user" : "fas fa-building",
},
},
];
} else {
newItems.push(item);
}
});
setState({ ...state, items: newItems });
};
useEffect(() => {
document.fonts.load('24px "Font Awesome 5 Free"').then(loadFontIcons);
}, []);
return (
<>
<LeftPanel name="reactchartfilter">
<Tooltip tooltipState={tooltipState} />
<Chart
ready={loadedChart}
data={state}
animateOnLoad={true}
options={{
logo: { u: "/images/Logo.png" },
handMode,
iconFontFamily: "Font Awesome 5 Free",
selectionColour: "#35495E",
hover: 0,
selectedNode: {
fbc: "rgba(0,0,0,0)",
},
selectedLink: {
fbc: "rgba(0,0,0,0)",
},
}}
containerClassName="klchart klchart-timebar"
selection-change={highlightItemsOnSelection}
hover={toggleTooltip}
drag-move={toggleTooltip}
click={updateHandMode}
/>
<Timebar
ready={loadedTimebar}
data={state}
selection={timebarSelection}
change={onTimebarChange}
animateOnLoad={true}
options={{
histogram: {
colour: "#61dbfb",
highlightColour: "#3e8496",
markColour: "rgb(192, 192, 192)",
markHiColour: "rgb(105, 105, 105)",
},
playSpeed: 30,
}}
containerClassName="kltimebar"
/>
</LeftPanel>
<RightPanel
name="reactchartfilter"
title="React Integration"
description="Use React and the time bar to filter chart items."
>
<fieldset>
<legend>Interaction</legend>
<ul>
<li>Use the time bar controls to see changes over time</li>
<li>
Hover/click a link to to show the duration of an individual's
directorship
</li>
<li>
Multi-select up to three items to highlight their neighbours
</li>
</ul>
</fieldset>
<fieldset>
<legend>Data Format</legend>
<p>
The time bar and chart data formats are similar, so you can pass the
same data object to both components.
</p>
</fieldset>
</RightPanel>
</>
);
};
// render our top-level component into the DOM
render(<ReactDemo />, document.querySelector(".chart-wrapper"));
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
<head>
<meta charset="UTF-8">
<title>React Integration</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="/reactchartfilter.css">
<link rel="preload" as="font" href="fonts/fontAwesome5/fa-solid-900.woff2" crossorigin="anonymous">
<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="/reactchartfilter-data.js" defer type="text/javascript"></script>
</head>
<body>
<div class="chart-wrapper demo-cols">
</div>
<div id="moreParent">
<div id="moreContainer">
</div>
</div>
<div id="lazyScripts">
<!-- This is a bundled JavaScript file.-->
<!-- React conventions are to include scripts after the target DOM node has loaded.-->
<script src="/reactchartfilter.js" defer type="text/javascript"></script>
</div>
</body>
</html>
//
// React components KeyLines v8.6.0-11643712880
//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
// This file provides React wrappers for the KeyLines chart and time bar
// You are welcome to make changes for better integration with your own projects
import React from 'react';
import KeyLines from 'keylines';
function invoke(fn, ...args) {
return (typeof fn === 'function') ? fn(...args) : undefined;
}
// This is the lowest level wrapper of the KeyLines integration - it deals with loading of the
// KeyLines component, options, resizing and raising KeyLines events up
function createKeyLinesComponent(type, onLoadFn) {
class KlComponent extends React.Component {
component = undefined;
componentLoading = false;
type = type;
onLoad = onLoadFn;
constructor(props) {
super(props);
// Bind our class specific functions to this
// See: https://facebook.github.io/react/docs/react-without-es6.html#autobinding
['applyProps', 'setSelection', 'onEvent', 'onLoad'].forEach((prop) => {
this[prop] = this[prop].bind(this);
});
}
componentDidMount() {
if (this.componentLoading) { return; }
this.componentLoading = true;
const componentDefinition = {
container: this.klElement,
type: this.type,
options: this.props.options,
};
KeyLines.create(componentDefinition).then((component) => {
this.componentLoading = false;
this.component = component;
component.on('all', this.onEvent);
this.applyProps().then(() => {
// Finally, tell the parent about the component so it can call functions on it
invoke(this.props.ready, component, this.klElement);
});
}).catch(() => {
this.componentLoading = false;
});
}
componentWillUnmount() {
if (this.component) {
// Clean up the component
this.component.destroy();
this.component = undefined;
}
}
setSelection() {
// This works because the selectionchange event is not raised when changing selection
// programmatically
const selectedItems = this.component.selection(this.props.selection);
if (this.type === 'chart' && selectedItems.length > 0) {
this.component.zoom('selection', { animate: true, time: 250 });
}
}
componentDidUpdate(prevProps) {
if (this.component) {
// we need to intercept the options being set and pass them on to the component manually
if (this.props.options && this.props.options !== prevProps.options) {
this.component.options(this.props.options); // don't worry about callback here
}
const reload = this.props.data !== prevProps.data;
if (reload) {
// note applyProps also deals with selection
return this.applyProps();
} else if (this.props.selection || prevProps.selection) {
if (this.props.selection !== prevProps.selection) {
this.setSelection();
}
}
}
}
// this looks for a handler with the right name on the props, and if it finds
// one, it will call it with the event arguments.
onEvent(props) {
return invoke(this.props[props.name], props.event);
}
// this applies all the component related props except the options which are
// handled differently
applyProps() {
return new Promise((resolve) => {
this.component.load(this.props.data).then(() => {
this.onLoad({ animate: !!this.props.animateOnLoad }).then(() => {
this.component.selection(this.props.selection);
resolve();
});
});
});
}
render() {
const { containerClassName, style, children } = this.props;
return (
<div ref={(klElement) => { this.klElement = klElement; }} className={containerClassName}
style={style}>
{children}
</div>
);
}
}
// defaultProps has to be a static property of the component class
KlComponent.defaultProps = {
data: {},
animateOnLoad: false,
options: {},
selection: [],
};
return KlComponent;
}
function createChartComponent() {
function onLoad(options) {
// default behaviour when loading data in the chart
return this.component.layout('organic', options);
}
return createKeyLinesComponent('chart', onLoad);
}
// Define the Chart component
export const Chart = createChartComponent();
function createTimebarComponent() {
function onLoad(options) {
// default behaviour when loading data in the timebar
return this.component.zoom('fit', options);
}
return createKeyLinesComponent('timebar', onLoad);
}
// Define the Timebar component
export const Timebar = createTimebarComponent();
//
// React demo components KeyLines v8.6.0-11643712880
//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
import React, { useEffect, useRef } from 'react';
/**
* @typedef {Object} RightPanelProps
* @property {string} name - The name of the demo.
* @property {string} title - The title of the demo.
* @property {string} description - The short description of the demo.
*/
/**
* @param {RightPanelProps} props
*/
const RightPanel = (props) => {
const sourceTab = useRef(null)
const aboutTab = useRef(null)
useEffect(() => {
const sourcePanel = document.getElementById('sourcePanel');
const aboutPanel = document.getElementById('aboutPanel');
if (sourcePanel && aboutPanel) {
// re-assign parent of child elements, rather than copy HTML so as to not remove any event handlers
sourceTab.current.appendChild(sourcePanel);
for(const child of Array.from(sourcePanel.children)) {
sourceTab.current.appendChild(child);
}
const aboutTemplate = aboutPanel.innerHTML;
aboutTab.current.innerHTML = aboutTemplate;
}
}, [])
return (
<div className="rhs citext flex flex-column">
<div className="title-bar">
<h3 className="flex1">{document.title.split('|')[0]}</h3>
<div className="buttons flex" id="titlebarbuttons">
<a className="btn btn-label disabled fshide" href="#" data-toggle="tooltip" id="fullscreenDisabled">
<i className="fa fa-compress"></i>
</a>
<a className="btn btn-label" href="#" data-toggle="tooltip" id="fullscreenButton">
<i className="fa fa-expand"></i>
</a>
</div>
</div>
<div className="notice">
<div className="boxout">
<div className="boxout-image">
<img src={`/images/${props.name}/ReGraph.png`}></img>
</div>
<p className="boxout-body" style={{ paddingLeft: 42, minHeight: 40 }}>Using React? Check out ReGraph, our React graph visualisation toolkit.
<a href="mailto:[email protected]">Contact us</a>
<span> or </span>
<a href="https://cambridge-intelligence.com/regraph/?utm_source=keylines&utm_medium=sdk-site&utm_campaign=keylines-sdk-referral-link">learn more</a>.
</p>
</div>
</div>
<div className="tabsdiv tab" data-tab-group="rhs">
<a className="btn btn-spaced fshide rhs-toggle fancy-button" id="closeRHS" href="#" data-toggle="tooltip">
<i className="fa fa-chevron-right"></i>
</a>
<button className="tablinks active" name="controlsTab" type="button">Controls</button>
<button className="tablinks" name="aboutTab" type="button">About</button>
<button className="tablinks" name="sourceTab" type="button">Source</button>
</div>
<div className="tab-content-panel flex1" data-tab-group="rhs">
<div className="toggle-content is-visible tabcontent" id="controlsTab">
<div className="cicontent">
<img className='kl-center' src={`/images/${props.name}/reactjs.png`} width='75%' />
{props.children}
</div>
</div>
<div ref={aboutTab} className="toggle-content tabcontent" id="aboutTab"></div>
<div ref={sourceTab} className="toggle-content tabcontent" id="sourceTab"></div>
</div>
</div>
);
}
/**
* @typedef {Object} LeftPanelProps
* @property {string} name
*/
/**
* @param {LeftPanelProps} props
*/
const LeftPanel = (props) => {
return (
<div className="flex1" id="lhs">
<div id="lhsVisual" className="toggle-content is-visible">
{props.children}
</div>
<div id="lhsSource" className="toggle-content">
<iframe id="sourceView" style={{ width: '100%', height: '100%', border: 'none' }} src={`source-${props.name}.js.htm`} scrolling="no"></iframe>
</div>
</div>
);
}
export { LeftPanel, RightPanel };
Loading source
