You can create your own HTML overlay to add elements made from scratch to your chart.
With KeyLines events, you can integrate these elements into the chart and make them respond to chart changes, view changes and user interactions.
The view-change event lets you scale the open notes with the chart when the view changes. In this case, the note size is also capped as the chart is zoomed in to keep the chart balanced and clear.
The drag-move event keeps the note position relative to the node when dragging the node and panning the chart.
Key functions used:
import KeyLines from "keylines";
import { data } from "./data.js";
let chart;
const baseAnnotationOffset = 35;
const annotations = {};
const chartContainer = document.getElementById("klchart");
const templateHtml = document.getElementById("annotation_html").innerHTML;
function createAnnotation(item) {
const itemId = item.id;
// Add details to the template
const html = templateHtml
.replace(/{{annotation-id}}/, itemId)
.replace(/{{close-id}}/, `${itemId}-close`)
.replace(/{{note}}/, item.d.notes);
// Add annotation to the DOM
const annotationContainer = createAnnotationContainer(itemId);
chartContainer.appendChild(annotationContainer);
annotationContainer.innerHTML = html;
// Update the annotations lookup state
annotations[itemId] = {
element: document.getElementById(`${itemId}`),
show: true,
};
// Allow annotations to be closed manually
document.getElementById(`${itemId}-close`).addEventListener("click", () => {
closeAnnotation(itemId);
});
}
function createAnnotationContainer(itemId) {
const newAnnotationContainer = document.createElement("div");
newAnnotationContainer.itemId = `annotation-container-${itemId}`;
newAnnotationContainer.setAttribute("data-id", itemId);
return newAnnotationContainer;
}
function updateAnnotationsPositions(zoomValue) {
const zoom = zoomValue || chart.viewOptions().zoom;
Object.keys(annotations).forEach((annotation) => {
if (annotations[annotation].show) {
const element = annotations[annotation].element;
const item = chart.getItem(annotation);
const point = chart.viewCoordinates(item.x + baseAnnotationOffset * (item.e || 1), item.y);
// Translate the annotation so its position corresponds to the point of the arrow
// and scale the size of the annotation depending on zoom level
element.style.transform = `translate(5px, -44px) scale(${Math.max(0.9, Math.min(2, zoom))}`;
element.style.left = `${point.x}px`;
element.style.top = `${point.y}px`;
}
});
}
function toggleStickyNoteGlyph(id, show) {
chart.setProperties({
id,
g: show
? [
{
p: "ne",
fi: {
t: "fas fa-sticky-note",
c: "#FEA95E",
},
e: 1.8,
},
]
: null,
});
}
function toggleAllAnnotations(showAll) {
Object.keys(annotations).forEach((itemId) => {
if (showAll) {
showAnnotation(itemId);
} else {
closeAnnotation(itemId);
}
});
}
function closeAnnotation(itemId) {
const annotation = annotations[itemId];
if (annotation.show) {
annotation.show = false;
toggleStickyNoteGlyph(itemId, true);
annotation.element.style.visibility = "hidden";
}
}
// Show the annotation and remove the indicative glyph
function showAnnotation(itemId) {
const annotation = annotations[itemId];
if (!annotation.show) {
annotations[itemId].show = true;
toggleStickyNoteGlyph(itemId, false);
annotations[itemId].element.style.visibility = "visible";
}
}
// Show the annotation of a selected item
function handleClick({ id }) {
if (id && annotations[id] && !annotations[id].show) {
showAnnotation(id);
updateAnnotationsPositions();
}
}
function initialiseInteractions() {
document.getElementById("showAnnotations").addEventListener("click", () => {
toggleAllAnnotations(true);
updateAnnotationsPositions();
});
document.getElementById("hideAnnotations").addEventListener("click", () => {
toggleAllAnnotations(false);
});
// Update all annotation positions on view change, but close them when the view dimensions change
chart.on("view-change", () => {
const { zoom } = chart.viewOptions();
updateAnnotationsPositions(zoom);
});
chart.on("click", handleClick);
// Keep the shown annotation position relative to the node
// when dragging the node and panning the chart
chart.on("drag-move", () => updateAnnotationsPositions());
}
async function startKeyLines() {
const options = {
handMode: true,
iconFontFamily: "Font Awesome 5 Free",
selectedNode: {
fc: "#000000",
},
};
chart = await KeyLines.create({ container: "klchart", options });
chart.load(data);
// If a node has a note, create an HTML annotation element for it
chart.each({ type: "node" }, (item) => {
if (item.d && item.d.notes) {
createAnnotation(item);
}
});
chart.layout("organic", { tightness: 3, consistent: true });
initialiseInteractions();
}
function loadWebFonts() {
document.fonts.load('24px "Font Awesome 5 Free"').then(startKeyLines);
}
window.addEventListener("DOMContentLoaded", loadWebFonts); import KeyLines from "keylines";
export const data = {
type: "LinkChart",
items: [
{
type: "node",
id: "node1",
t: "Harry Barnes",
d: {
notes: "Prior conviction for financial crime in 2017.",
},
fi: {
t: "far fa-user",
c: "#397af2",
},
tc: false,
fc: "#666a6d",
fb: true,
fs: 12,
},
{
type: "node",
id: "node2",
t: "Contract 202005-ZA",
fi: {
t: "far fa-handshake",
c: "#000000",
},
tc: false,
fc: "#666a6d",
fb: true,
b: "#000000",
bw: 1,
fs: 12,
},
{
type: "node",
id: "node9",
t: "Contract 201905-GB",
fi: {
t: "far fa-handshake",
c: "#000000",
},
tc: false,
fc: "#666a6d",
fb: true,
b: "#000000",
bw: 1,
fs: 12,
},
{
type: "node",
id: "node3",
t: "Brooke Webb",
fi: {
t: "far fa-user",
c: "#397af2",
},
tc: false,
fc: "#666a6d",
fb: true,
fs: 12,
},
{
type: "node",
id: "node4",
t: "ABC Ltd",
fi: {
t: "far fa-building",
c: "#000000",
},
tc: false,
fc: "#666a6d",
fb: true,
b: "#000000",
bw: 1,
fs: 12,
},
{
type: "node",
id: "node5",
t: "Rory Hopkins",
fi: {
t: "far fa-user",
c: "#397af2",
},
tc: false,
fc: "#666a6d",
fb: true,
fs: 12,
},
{
type: "node",
id: "node6",
t: "Lynn Young",
d: {
notes: "Currently under investigation for contract 201904-JS.",
},
fi: {
t: "far fa-user",
c: "#397af2",
},
tc: false,
fc: "#666a6d",
fb: true,
fs: 12,
},
{
type: "node",
id: "node7",
t: "XYZ Ltd",
fi: {
t: "far fa-building",
c: "#000000",
},
tc: false,
fc: "#666a6d",
fb: true,
b: "#000000",
bw: 1,
fs: 12,
},
{
type: "link",
id: "node1-node4",
id1: "node1",
id2: "node4",
},
{
type: "link",
id: "node4-node3",
id1: "node4",
id2: "node3",
},
{
type: "link",
id: "node4-node2",
id1: "node4",
id2: "node2",
},
{
type: "link",
id: "node7-node5",
id1: "node5",
id2: "node7",
},
{
type: "link",
id: "node2-node6",
id1: "node2",
id2: "node6",
},
{
type: "link",
id: "node2-node7",
id1: "node2",
id2: "node7",
},
{
type: "link",
id: "node6-node9",
id1: "node6",
id2: "node9",
},
{
type: "link",
id: "node3-node9",
id1: "node3",
id2: "node9",
},
],
}; <!doctype html>
<html lang="en" style="background-color: #2d383f">
<head>
<meta charset="utf-8" />
<title>HTML Overlays</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" />
<link
rel="stylesheet"
type="text/css"
href="@fortawesome/[email protected]/css/fontawesome.css"
/>
<link
rel="stylesheet"
type="text/css"
href="@fortawesome/[email protected]/css/solid.css"
/>
<link
rel="stylesheet"
type="text/css"
href="@fortawesome/[email protected]/css/regular.css"
/>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div id="klchart" class="klchart"></div>
<template id="annotation_html"
><div
class="annotation"
id="{{annotation-id}}"
style="margin: 0px; transform-origin: left; position: absolute; min-width: 330px"
>
<div class="arrow"></div>
<h2 class="annotation-title">
<strong>Notes</strong
><button class="close-button" type="submit" id="{{close-id}}">
<i class="fa fa-times"></i>
</button>
</h2>
<div class="annotation-content">{{note}}</div>
</div></template
>
<script type="module" src="./code.js"></script>
</body>
</html> .klchart {
overflow: hidden;
}
/* Annotation styling */
.annotation {
background: #ffffff;
pointer-events: none;
}
.annotation-title {
background-color: #fea95e;
border: 1px solid #808080;
border-bottom: none;
font-size: 14px;
padding: 8px 14px;
margin: 0px;
line-height: 20px;
top: 0px;
}
.annotation-content {
border: 1px solid #808080;
font-size: 12px;
padding: 8px 14px;
}
.annotation .arrow {
background: #ffffff;
border-bottom: 1px solid #808080;
border-left: 1px solid #808080;
transform: translate(-5px, 39px) rotateZ(45deg);
width: 10px;
height: 10px;
position: absolute;
}
.annotation .close-button {
background-color: #fea95e;
height: 15px;
width: 15px;
padding: 0px 0px;
line-height: 17px;
float: right;
pointer-events: all;
}
.annotation .fa-times {
color: #000000;
}