//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
// Sample Code
//
//! Create fully custom HTML overlays for the chart.
import KeyLines from "keylines";
import { data } from "./overlayhtml-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);
<!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="/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/fontawesome5/regular.css">
<link rel="stylesheet" type="text/css" href="/overlayhtml.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="/overlayhtml.js" crossorigin="use-credentials" defer type="module"></script>
</head>
<body>
<div class="chart-wrapper demo-cols">
<div class="tab-content-panel flex1" id="lhs" data-tab-group="rhs">
<div class="toggle-content is-visible" id="lhsVisual" style="width:100%; height: 100%;">
<div class="klchart" id="klchart">
</div>
</div>
</div>
<div class="rhs citext closed" id="demorhscontent">
<div class="title-bar">
<svg viewBox="0 0 360 60" style="max-width: 360px; max-height: 60px; font-size: 24px; font-family: Raleway; flex: 1;">
<text x="0" y="38" style="width: 100%;">HTML Overlays</text>
</svg>
</div>
<div class="tab-content-panel" data-tab-group="rhs">
<div class="toggle-content is-visible tabcontent" id="controlsTab">
<form autocomplete="off" onsubmit="return false" id="rhsForm">
<p>Use the <i class="fa fa-times"></i> and <i class="fa fa-sticky-note" style="color: #FEA95E"></i> controls or the buttons below to toggle closing and opening the notes.</p>
<p>
Pan and zoom the chart to see how open notes respond to view changes.
</p>
<div class="cicontent">
<fieldset>
<input class="btn btn-block" type="button" value="Open all sticky notes" id="showAnnotations">
<input class="btn btn-block" type="button" value="Close all sticky notes" id="hideAnnotations">
</fieldset>
</div>
</form>
</div>
</div>
</div>
</div>
<div id="moreParent">
<div id="moreContainer">
<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>
</div>
</div>
<div id="lazyScripts">
</div>
</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;
}
Loading source
