// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
// Sample Code
//! Use Angular to create time bar and chart with font icons and tooltips.
// This demo uses webpack to manage dependencies and build
import KeyLines from "keylines";
import "core-js";
import "zone.js";
import { OnInit } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { BrowserModule } from "@angular/platform-browser";
import { NgModule, enableProdMode, Component } from "@angular/core";
import type {
Chart,
TimeBar,
TimeBarOptions,
ChartOptions,
ChartAllEventProps,
ChartPointerEventProps,
SelectionOptions,
LayoutOptions,
TimeBarAllEventProps,
} from "keylines";
// import the KeyLines components
import {
KlComponent,
KlComponents,
KlComponentsService,
} from "angular/angular-keylines";
import { LeftPanel, RightPanel } from "angular/angular-demo";
import { chartData, timebarData } from "./angularchartfilter-data";
// enable production mode
enableProdMode();
interface Events {
name: string;
args: ChartAllEventProps | TimeBarAllEventProps;
}
export class Tooltip {
constructor(
public start: string,
public end: string,
public left: string,
public top: string,
public tooltipClass: string,
public arrowClass: string
) {}
}
@Component({
selector: "kl-container",
template: `
<div class="chart-wrapper demo-cols">
<kl-leftPanel class="flex1">
<span
#ngTooltip
id="tooltip"
class="{{ this.model.tooltipClass }}"
[style.left]="this.model.left"
[style.top]="this.model.top"
>
<div class="{{ this.model.arrowClass }}"></div>
<tr>
<td class="table-cell"><b>Appointed on: </b></td>
<td>{{ this.model.start }}</td>
</tr>
<tr>
<td class="table-cell"><b>Resigned on: </b></td>
<td>{{ this.model.end }}</td>
</tr>
</span>
<div id="lhsVisual" class="toggle-content is-visible">
<kl-components (klReady)="klChartReady($event)">
<kl-component
class="flex1"
klType="chart"
klContainerClass="klchart klchart-timebar"
[klOptions]="chartOptions"
(klEvents)="klChartEvents($event)"
>
</kl-component>
<kl-component
klType="timebar"
klContainerClass="kltimebar"
[klOptions]="timebarOptions"
(klEvents)="klTimebarEvents($event)"
>
</kl-component>
</kl-components>
</div>
<div id="lhsSource" class="toggle-content">
<iframe
id="sourceView"
style="width: 100%; height: 100%; border: none;"
src="source-angularchartfilter.js.htm"
scrolling="no"
></iframe>
</div>
</kl-leftPanel>
<kl-rightPanel
name="angularchartfilter"
title="Angular Integration: Time"
description="Use Angular 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 data format is similar to that used for the chart. It
is possible to pass the same data object to both components.
</p>
</fieldset>
</kl-rightPanel>
</div>
`,
styleUrls: ["angularchartfilter.css"],
})
export class ChartFilterComponent implements OnInit {
private chart?: Chart;
private timebar?: TimeBar;
// Define some options for the chart
public chartOptions: ChartOptions = {
logo: { u: "/images/Logo.png" },
overview: { icon: false, shown: false },
iconFontFamily: "Font Awesome 5 Free",
handMode: true,
hover: 0,
selectedNode: {
fbc: "rgba(0,0,0,0)",
},
selectedLink: {
fbc: "rgba(0,0,0,0)",
},
};
// Define some options for the timebar
public timebarOptions: TimeBarOptions = {
histogram: {
colour: "#E64669",
highlightColour: "#DD0031",
markColour: "rgb(192, 192, 192)",
markHiColour: "rgb(105, 105, 105)",
},
};
model = new Tooltip("", "", "", "", "hidden", "arrow");
marks = [
{
dt1: new Date("01 Jan 2018 01:00:00"),
dt2: new Date("01 Jan 2050 01:00:00"),
},
];
greyColours = ["rgb(105, 105, 105)", "rgb(192, 192, 192)"];
// only style the first 3 selections
selectionColours = [
["#9600d5", "#d0a3cb"],
["rgb(0, 191, 255)", "#8dd8f8"],
["rgb(50,205,50)", "#afdaae"],
];
ngOnInit() {
document.fonts.load('24px "Font Awesome 5 Free"');
}
klChartReady([chart, timebar]: [Chart, TimeBar]) {
this.chart = chart;
this.timebar = timebar;
chartData.items.forEach((element) => {
if (element.type === "node") {
element.fi = {
c: this.greyColours[0],
t: element.d.type === "person" ? "fas fa-user" : "fas fa-building",
};
}
});
this.chart.load(chartData);
this.timebar.load(timebarData);
this.timebar.mark(this.marks);
this.chart.zoom("fit");
this.timebar.zoom("fit");
}
// will receive events for the timebar
klTimebarEvents({ name }: Events) {
if (name === "change") {
this.onTimebarChange();
}
}
// will receive events for the chart
klChartEvents({ name, args }: Events) {
if (name === "selection-change" && this.chart?.selection()) {
this.highlightSelections();
} else if (
name === "hover" ||
name === "pointer-down" ||
name === "drag-move"
) {
this.toggleTooltip(args as ChartPointerEventProps);
}
}
getDateFromTimestamp(timestamp: number) {
return timestamp ? new Date(timestamp).toDateString().slice(4) : "n/a";
}
toggleTooltip({ id, x, y }: ChartPointerEventProps) {
if (this.chart) {
const item = id ? this.chart.getItem(id) : null;
const viewWidth = this.chart.viewOptions().width;
if (item && item.type === "link") {
const timebarItem = timebarData.items.filter(
(tbItem) => tbItem.id === item.id
) as { dt: { dt1: number; dt2: number }[] }[];
this.model.start = this.getDateFromTimestamp(timebarItem[0].dt[0].dt1);
this.model.end = this.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) {
this.model.top = `${y - 24}px`;
this.model.left = `${x + 22}px`;
this.model.tooltipClass = "";
} else {
this.model.top = `${y - 24}px`;
this.model.left = `${x - 218}px`;
this.model.tooltipClass = "right";
}
} else {
this.model.tooltipClass = "hidden";
}
}
}
buildItemStyling(item: any, width: number, colour: string, 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 || 4, c: colour };
}
highlightSelections() {
if (!this.chart || !this.timebar) return;
this.timebar.selection([]);
const timebarSelection: SelectionOptions[] = [];
const chartProperties: Record<
string,
ReturnType<typeof this.buildItemStyling>
> = {};
// set default colouring
this.chart.each({ type: "all" }, (item) => {
chartProperties[item.id] = this.buildItemStyling(
item,
3,
item.type === "node" ? this.greyColours[0] : this.greyColours[1]
);
});
let selectedIds = this.chart.selection();
if (selectedIds) {
if (selectedIds.length > this.selectionColours.length) {
selectedIds = selectedIds.slice(0, this.selectionColours.length);
this.chart.selection(selectedIds);
}
selectedIds.forEach((id, index) => {
let neighbouringNodes: any = [];
let links: string[] = [];
const item = this.chart?.getItem(id);
const colour =
index < this.selectionColours.length
? this.selectionColours[index][0]
: this.greyColours[1];
const linkColour = this.selectionColours[index][1];
if (item && item.type === "node") {
// all: true - applies styles to hidden nodes + links not in current time range
const neighbours = this.chart?.graph().neighbours(id, { all: true });
if (neighbours && neighbours.links) {
links = neighbours.links;
neighbouringNodes = neighbours.nodes;
}
// colour node
chartProperties[id] = this.buildItemStyling(item, 3, colour, true);
links.forEach((link: string) => {
const linkItem = this.chart?.getItem(link);
chartProperties[link] = this.buildItemStyling(
linkItem,
5,
linkColour
);
});
} else if (item && item.type === "link") {
links.push(id);
neighbouringNodes.push(item.id1, item.id2);
// colour link
chartProperties[id] = this.buildItemStyling(item, 5, colour);
}
// colour neighbour nodes
neighbouringNodes.forEach((neighbourId: string) => {
if (
chartProperties[neighbourId].fi.c === this.greyColours[0] &&
index < this.selectionColours.length
) {
chartProperties[neighbourId] = this.buildItemStyling(
this.chart?.getItem(neighbourId),
3,
this.selectionColours[index][1]
);
}
});
// and select in the timebar
timebarSelection.push({
id: links,
index,
c: colour,
});
});
this.timebar.selection(timebarSelection);
this.chart.setProperties(
Object.keys(chartProperties).map((key) => chartProperties[key])
);
}
}
doLayout() {
if (!this.chart) return;
const opts: LayoutOptions = {
time: 500,
mode: "adaptive",
easing: "linear",
straighten: false,
};
this.chart.layout("organic", opts);
}
onTimebarChange() {
if (!this.chart || !this.timebar) return;
// filter the chart to show only items in the new range
this.chart.filter(this.timebar.inRange, { animate: false, type: "link" });
// and then adjust the chart's layout
this.doLayout();
}
}
@NgModule({
imports: [BrowserModule],
declarations: [
ChartFilterComponent,
KlComponent,
KlComponents,
LeftPanel,
RightPanel,
],
providers: [KlComponentsService],
bootstrap: [ChartFilterComponent],
})
export class AppModule {}
platformBrowserDynamic().bootstrapModule(AppModule);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
<head>
<meta charset="UTF-8">
<title>Angular 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">
<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="/vendor/jquery-ui.js" defer type="text/javascript"></script>
<script src="/vendor/Reflect.js" defer type="text/javascript"></script>
</head>
<body>
<div class="chart-wrapper demo-cols"><kl-container id="kl-container" class="flex1 flex">
</kl-container>
</div>
<div id="moreParent">
<div id="moreContainer">
</div>
</div>
<div id="lazyScripts">
<!-- This is a bundled JavaScript file.-->
<!-- Angular conventions are to include scripts after the target DOM node has loaded.-->
<script src="angularchartfilter.js" crossorigin="use-credentials" defer type="module"></script>
</div>
</body>
</html>
#tooltip {
position: absolute;
padding: 5px;
display: block;
color: black;
float: left;
border: 1px solid #ccc;
background-color: #fff;
z-index: 1000;
width: 208px;
}
.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 {
top: 20px;
left: 199px;
transform: rotateZ(225deg);
}
.table-cell {
text-align: right;
padding: 5px;
}
//
// Angular components KeyLines v8.6.0-11643712880
//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
import {
Component, ElementRef, Input, Output, EventEmitter, Injectable, AfterViewInit, OnDestroy,
OnChanges, SimpleChange, ContentChildren, ViewChild, NgZone
} from '@angular/core';
import type * as kl from 'keylines';
import KeyLines from 'keylines';
@Injectable()
export class KlComponentsService {
constructor(private ngZone: NgZone) {}
create(
componentDefinitions: kl.Component[],
pathToImages: string
): Promise<(kl.Chart | kl.TimeBar)[]> {
// KeyLines paths configuration
KeyLines.paths({ images: pathToImages });
// KeyLines create components, running the KeyLines create call outside Angular
// to prevent call of requestAnimationFrame from KeyLines triggering Angular change detection.
return this.ngZone.runOutsideAngular(() => {
return KeyLines.create(componentDefinitions);
});
}
}
@Component({
selector: 'kl-component',
template: '<div #container [ngClass]="containerClass" [ngStyle]="style"></div>'
})
export class KlComponent implements OnChanges, OnDestroy {
@Input() id: string = ""; //optional
@Input('ngStyle') style: any; //optional
@Input('klType') type: "chart" | "timebar" = "chart"; // optional
@Input('klOptions') options: kl.ChartOptions | kl.TimeBarOptions = {}; // optional
@Input('klContainerClass') containerClass: string = ""; // optional
@Output('klReady') klReady = new EventEmitter(); // optional
@Output('klEvents') klEvents = new EventEmitter(); // optional
// Save the reference of the container element: see #container in the template
@ViewChild('container', { static: false })
private containerElement?: ElementRef;
// The KeyLines component
private component?: kl.Chart | kl.TimeBar;
isChart(component: kl.Chart | kl.TimeBar): component is kl.Chart {
return this.type === "chart";
}
// lifecycle hooks
ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
const { options } = changes;
// Refresh the options when necessary
if (options && !options.isFirstChange()) {
this.refreshOptions(options.currentValue);
}
}
ngOnDestroy() {
if (this.component) {
// ensure the component cleans up its resources
this.component.destroy();
}
}
// Kl instructions
getHeader(): kl.Component {
return {
container: this.containerElement ? this.containerElement.nativeElement : undefined,
type: this.type,
options: this.options
};
}
setUpComponent(component: kl.Chart | kl.TimeBar) {
// save the reference of the component
this.component = component;
// trigger a klReady event with the component reference
this.klReady.emit(component);
// attach the component events
this.registerEvent();
}
registerEvent() {
function emitEvent(this: KlComponent, props: any): void {
const klEvent = { name: props.name, args: props.event, preventDefault: false };
// dispatch the event to the parent
this.klEvents.emit(klEvent);
if (klEvent.preventDefault && props.event && props.event.preventDefault) {
props.event.preventDefault();
}
}
if (this.component) {
if (this.isChart(this.component)) {
this.component.on('all', emitEvent.bind(this));
} else {
this.component.on('all', emitEvent.bind(this));
}
}
}
refreshOptions(options: kl.ChartOptions | kl.TimeBarOptions) {
if (this.component) {
// Use type guard to allow TypeScript to infer type and prevent errors
if (this.isChart(this.component)) {
this.component.options(options);
}
else {
this.component.options(options);
}
}
}
}
@Component({
selector: 'kl-components',
template: '<ng-content></ng-content>'
})
export class KlComponents implements AfterViewInit {
@Input('klImagesPath') pathToImages = ''; // optional
@Output('klReady') klReady = new EventEmitter(); // optional
// save the KeyLines service
private KlComponentsService: KlComponentsService;
// get the list of the children components
// http://blog.thoughtram.io/angular/2015/09/03/forward-references-in-angular-2.html
@ContentChildren(KlComponent)
private components?: KlComponent[];
// constructor
constructor(KlComponentsService: KlComponentsService) {
this.KlComponentsService = KlComponentsService;
}
// lifecycle hooks
ngAfterViewInit() {
if (!this.components) throw 'Could not find kl-component declaration';
// iterate over the list of children components to create the KeyLines definition of components
const componentDefinitions = this.components.map((component) => component.getHeader());
this.createComponents(componentDefinitions);
}
// KL instructions
createComponents(componentDefinitions: kl.Component[]) {
// use the KeyLines service to create the components
this.KlComponentsService.create(componentDefinitions, this.pathToImages)
.then((components) => this.notifyComponents(components))
.catch((error: any) => error);
}
notifyComponents(components: (kl.Chart | kl.TimeBar)[] | kl.Chart | kl.TimeBar) {
// ensure that we have an array of components
if (!Array.isArray(components)) {
components = [components];
}
this.klReady.emit(components);
// finalise the set up of registered components
if (this.components){
this.components.forEach((component, index) => {
component.setUpComponent((components as (kl.Chart | kl.TimeBar)[])[index])
});
}
}
}
//
// Angular demo components KeyLines v8.6.0-11643712880
//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
import { Component, Input, ViewChild, ElementRef } from '@angular/core';
@Component({
selector: 'kl-leftPanel',
template: `
<ng-content></ng-content>`,
})
export class LeftPanel {}
@Component({
selector: 'kl-rightPanel',
template: `
<div class="rhs citext flex flex-column">
<div class="title-bar">
<h3 class="flex1">Angular Integration</h3>
<div class="buttons flex" id="titlebarbuttons">
<a class="btn btn-label disabled fshide" href="#" data-toggle="tooltip" id="fullscreenDisabled">
<i class="fa fa-compress"></i>
</a>
<a class="btn btn-label" href="#" data-toggle="tooltip" id="fullscreenButton">
<i class="fa fa-expand"></i>
</a>
</div>
</div>
<div class="tabsdiv tab" data-tab-group="rhs">
<a class="btn btn-spaced fshide rhs-toggle fancy-button" id="closeRHS" href="#" data-toggle="tooltip">
<i class="fa fa-chevron-right"></i>
</a>
<button class="tablinks active" name="controlsTab" type="button">Controls</button>
<button class="tablinks" name="aboutTab" type="button">About</button>
<button class="tablinks" name="sourceTab" type="button">Source</button>
</div>
<div class="tab-content-panel flex1" data-tab-group="rhs">
<div class="toggle-content is-visible tabcontent" id="controlsTab">
<div class="cicontent">
<img class='kl-center' src='images/{{ name }}/angular.png' width='40%' />
<ng-content></ng-content>
</div>
</div>
<div id="aboutTab" #aboutTab class="toggle-content tabcontent"></div>
<div id="sourceTab" #sourceTab class="toggle-content tabcontent"></div>
</div>
</div>
`,
})
export class RightPanel {
@Input() name: string = '';
@Input() title: string = '';
@Input() description: string = '';
@ViewChild('sourceTab', { static: false }) sourceTabEl?: ElementRef;
@ViewChild('aboutTab', { static: false }) aboutTabEl?: ElementRef;
ngAfterViewInit() {
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
this.sourceTabEl!.nativeElement.appendChild(sourcePanel);
for (const child of Array.from(sourcePanel.children)) {
this.sourceTabEl!.nativeElement.appendChild(child);
}
const aboutTemplate = aboutPanel.innerHTML;
this.aboutTabEl!.nativeElement.innerHTML = aboutTemplate;
// @ts-ignore
if (window.setupTabPanel) window.setupTabPanel();
}
}
}
Loading source
