//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
// Sample Code
//! Use Vue and the time bar to filter chart items.
import KeyLines from "keylines";
import { createApp } from "vue";
import Demo from "./vuechartfilter.vue";
createApp(Demo).mount("#kl-container");
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
<head>
<meta charset="UTF-8">
<title>Vue.js 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="/css/font-awesome.css">
<link rel="stylesheet" type="text/css" href="../vuechartfilter.css">
<script src="/vendor/jquery.js" defer type="text/javascript"></script>
<script id="keylines" src="/public/keylines.umd.cjs" defer type="text/javascript"></script>
</head>
<body>
<div class="chart-wrapper demo-cols"><kl-container id="kl-container" class="chart-wrapper demo-cols">
</kl-container>
</div>
<div id="moreParent">
<div id="moreContainer">
</div>
</div>
<div id="lazyScripts">
<script src="/vuechartfilter.js" crossorigin="use-credentials" type="module"></script>
</div>
</body>
</html>
//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
<template>
<div class="chart-wrapper demo-cols">
<left-panel name="vuechartfilter">
<span id="tooltip" :class="tooltip.tooltipClass"
:style="{ left: tooltip.left, top: tooltip.top }">
<div :class="tooltip.arrowClass"></div>
<tr>
<td class="table-cell"><b>Appointed on: </b></td>
<td>{{ tooltip.start }}</td>
</tr>
<tr>
<td class="table-cell"><b>Resigned on: </b></td>
<td>{{ tooltip.end }}</td>
</tr>
</span>
<kl-chart
id="kl"
containerClass="klchart klchart-timebar"
:animateOnLoad="true"
:data="data"
:options="chartOptions"
@kl-ready="onChartReady"
@kl-selection-change="highlightSelections"
@kl-hover="toggleTooltip"
@kl-drag-move="toggleTooltip"
/>
<kl-timebar
id="timebar"
containerClass="kltimebar"
:animateOnLoad="true"
:data="data"
:options="timebarOptions"
@kl-change="onTimebarChange"
@kl-ready="onTimebarReady"
/>
</left-panel>
<right-panel
title="Vue.js Integration"
name="vuechartfilter"
description="Use Vue 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>
</right-panel>
</div>
</template>
<script>
import KlChart from '../../../../vue/Chart.vue';
import KlTimebar from '../../../../vue/Timebar.vue';
import LeftPanel from '../../../../vue/LeftPanel.vue';
import RightPanel from '../../../../vue/RightPanel.vue';
import KeyLines from "keylines"
import data from '../vuechartfilter-data';
export default {
name: 'demo',
components: {
LeftPanel,
RightPanel,
KlChart,
KlTimebar,
},
data: () => ({
chartOptions: {
logo: { u: '/images/Logo.png' },
handMode: true,
iconFontFamily: 'Font Awesome 5 Free',
selectionColour: '#35495E',
hover: 0,
selectedNode: {
fbc: 'rgba(0,0,0,0)'
},
selectedLink: {
fbc: 'rgba(0,0,0,0)'
}
},
timebarOptions: {
histogram: {
colour: '#42B883',
highlightColour: '#35495E',
markColour: 'rgb(192, 192, 192)',
markHiColour: 'rgb(105, 105, 105)',
},
playSpeed: 30,
},
greyColours: ['rgb(105, 105, 105)', 'rgb(192, 192, 192)'],
// only style the first 3 selections
selectionColours: [
['#9600d5', '#d0a3cb'],
['#0044ff', '#6b93ff'],
['#ff0000', '#ff9e9e'],
],
marks: [
{
dt1: new Date('01 Jan 2018 01:00:00'),
dt2: new Date('01 Jan 2050 01:00:00')
}
],
data,
tooltip: {
start: '',
end: '',
left: '',
top: '',
tooltipClass: 'hidden',
arrowClass: 'arrow'
}
}),
mounted() {
document.fonts.load('24px "Font Awesome 5 Free"'),
// set font icons for each node type
this.data.items.forEach((item) => {
if (item && item.type === 'node') {
item.fi = {
c: this.greyColours[0],
t: item.d.type === 'person' ? 'fas fa-user' : 'fas fa-building',
};
}
})
},
methods: {
onChartReady(chart) {
this.chart = chart;
},
onTimebarReady(timebar) {
this.timebar = timebar;
this.timebar.mark(this.marks);
},
onTimebarChange() {
if (this.chart && this.timebar) {
// filter the chart to show only items in the new range
this.chart.filter(this.timebar.inRange, { animate: false, type: 'link' }).then(() => {
this.chart.layout('organic', { time: 500, mode: 'adaptive', easing: 'linear' });
});
}
},
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 };
},
highlightSelections() {
if (!this.chart || !this.timebar) return;
const timebarSelection = [];
const chartProperties = {};
// 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 = [];
let links = [];
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) => {
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) => {
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]));
}
},
getDateFromTimestamp(timestamp) {
return timestamp ? new Date(timestamp).toDateString().slice(4) : 'n/a';
},
toggleTooltip({ id, x, y }) {
if (this.chart) {
const item = id ? this.chart.getItem(id) : null;
const viewWidth = this.chart.viewOptions().width;
if (item && item.type === 'link') {
const timebarItem = data.items.filter((tbItem) => {
if (tbItem.id === item.id) {
return tbItem;
}
})
this.tooltip.start = this.getDateFromTimestamp(timebarItem[0].dt[0].dt1);
this.tooltip.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.tooltip.top = `${y - 24}px`;
this.tooltip.left = `${x + 22}px`;
this.tooltip.tooltipClass = '';
} else {
this.tooltip.top = `${y - 24}px`;
this.tooltip.left = `${x - 218}px`;
this.tooltip.tooltipClass = 'right';
}
} else {
this.tooltip.tooltipClass = 'hidden';
}
}
}
},
};
</script>
//
// Vue components KeyLines v8.6.0-11643712880
//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
<template>
<div ref="container" :class="containerClass" :style="styleObject"></div>
</template>
<script>
import KeyLines from 'keylines';
export default {
name: "KlComponent",
props: {
id: {
type: String,
required: true
},
container: Object,
containerClass: String,
styleObject: Object,
options: Object,
data: Object,
animateOnLoad: {
type: Boolean,
default: false
},
selection: {
type: Array,
default: () => []
}
},
mounted() {
KeyLines.create({
id: this.id,
options: this.options,
container: this.$refs ? this.$refs.container : null,
type: this.type
}).then(component => {
this.klcreate(component);
}).catch(console.err);
},
beforeUnmount() {
if (this.component) {
this.component.destroy();
}
},
methods: {
onEvent(props) {
const name = 'kl-' + props.name;
this.$emit('kl-all', props);
this.$emit(name, props.event);
},
klcreate(component) {
this.component = component;
this.component.on('all', this.onEvent);
this.component.load(this.data)
.then(() => this.onLoad({ animate: !!this.animateOnLoad }))
.then(() => {
component.selection(this.selection);
this.$emit('kl-ready', component);
});
}
}
};
</script>
//
// Vue components KeyLines v8.6.0-11643712880
//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
<script>
import KlComponent from './Keylines.vue';
export default {
name: 'KlChart',
extends: KlComponent,
data: () => ({
type: 'chart'
}),
methods: {
onLoad(options) {
return this.component.layout('organic', options);
}
}
};
</script>
//
// Vue components KeyLines v8.6.0-11643712880
//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
<script>
import KlComponent from './Keylines.vue';
export default {
name: 'KlTimebar',
extends: KlComponent,
data: () => ({
type: 'timebar'
}),
methods: {
onLoad() {
return this.component.zoom('fit');
}
}
};
</script>
//
// Vue demo components KeyLines v8.6.0-11643712880
//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
<script>
const leftPanelTemplate = `
<div class="flex1" id="lhs">
<div id="lhsVisual" style="height: 99%;" class="toggle-content is-visible">
<slot></slot>
</div>
<div id="lhsSource" class="toggle-content"/>
</div>
`;
export default {
name: 'LeftPanel',
props: {
name: String
},
mounted() {
const sourceCodeElement = document.getElementById('lhsSource');
const sourceCode = `<iframe id="sourceView" style="width: 100%; height: 100%; border: none;" src="source-${this.name}.js.htm" scrolling="no"></iframe>`;
sourceCodeElement.innerHTML = sourceCode;
},
template: leftPanelTemplate
};
</script>
//
// Vue demo components KeyLines v8.6.0-11643712880
//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
<script>
const rightPanelTemplate = `
<div class="rhs citext flex flex-column">
<div class="title-bar">
<h3 class="flex1">${document.title.split('|')[0]}</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 vue-logo" :src="image.src" :alt="image.alt" />
<slot></slot>
</div>
</div>
<div ref="aboutTab" class="toggle-content tabcontent" id="aboutTab"></div>
<div ref="sourceTab" class="toggle-content tabcontent" id="sourceTab"></div>
</div>
</div>
`;
export default {
name: 'RightPanel',
props: {
title: String,
name: String,
description: String,
isFullscreenAvailable: {
type: Boolean,
default: () => typeof doFullScreen === 'function'
}
},
mounted() {
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.$refs.sourceTab.appendChild(sourcePanel);
for(const child of Array.from(sourcePanel.children)) {
this.$refs.sourceTab.appendChild(child);
}
const aboutTemplate = aboutPanel.innerHTML;
this.$refs.aboutTab.innerHTML = aboutTemplate;
}
},
computed: {
image() {
return {
src: `images/${this.name}/vuejs.png`,
alt: 'Vue.js logo'
};
}
},
template: rightPanelTemplate
};
</script>
Loading source
