//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
// Sample Code
//
//! Create annotations with labels, buttons and interactive controls.
import {
data,
itemsMap,
colours,
themeAnnotations,
threadAnnotations,
icons,
showAnnotationGlyph,
} from "./advancedannotationstyles-data.js";
import KeyLines from "keylines";
let chart;
const subItemTypePropMap = {
glyph: "g",
label: "t",
};
/**
* Contains the data and click handlers needed to manage and process user interactions with chart items
* Sub items (labels / glyphs) act as buttons
* - an onClick handler is specified for each button and called via the chart 'click' event
* - a hover style is specified for each hoverable button and applied using chart.setProperties() via the chart 'hover' event
* The lastHoveredButton property stores the item id and subItem id of the most recently hovered button
* so that the hover effect can be removed when it is unhovered
*/
const interactionInfo = {
// store the last hovered button's parent id and subItemIndex for style updates
lastHoveredButton: null,
items: {
a4: {
subItemType: "glyph",
buttons: {
0: {
onClick: async () => await toggleShowHideAnnotation("hide"),
styles: {
default: {
c: colours.a4.defaultButton,
b: colours.a4.defaultButton,
},
hovered: {
c: colours.a4.hoveredButton,
b: colours.a4.hoveredButton,
},
},
},
},
},
n4: {
subItemType: "glyph",
buttons: {
0: {
onClick: async () => await toggleShowHideAnnotation("show"),
styles: {
default: {
fi: {
t: icons.comment,
c: colours.a4.defaultButton,
},
},
hovered: {
fi: {
t: icons.comment,
c: colours.a4.hoveredButton,
},
},
},
},
},
},
a5: {
subItemType: "label",
buttons: {
1: {
onClick: async () => await updateAnnotation(themeAnnotations.light),
styles: {
default: {
fbc: colours.a5.defaultButton,
fc: "rgb(0, 0, 0)",
},
hovered: {
fbc: colours.a5.hoveredButton,
fc: "rgb(255, 255, 255)",
},
},
},
2: {
onClick: async () => await updateAnnotation(themeAnnotations.dark),
styles: {
default: {
fbc: colours.a5.defaultButton,
fc: "rgb(0, 0, 0)",
},
hovered: {
fbc: colours.a5.hoveredButton,
fc: "rgb(255, 255, 255)",
},
},
},
},
},
a6: {
subItemType: "label",
buttons: {
5: {
onClick: async () => await updateAnnotation(threadAnnotations.open),
},
10: {
onClick: async () => await updateAnnotation(threadAnnotations.closed),
},
},
},
},
};
/* Formats the code snippets neatly in the display box on the right-hand side
if no annotation is provided, prompt the user to select an annotation */
function prettyPrint(item) {
const codeDisplayEl = document.getElementById("display");
if (item?.type === "annotation") {
const json = JSON.stringify(item, undefined, 1);
codeDisplayEl.textContent = json;
} else {
codeDisplayEl.textContent = "Click an annotation";
}
}
// Update the itemsMap so we can prettyPrint only the required properties of the annotation
async function updateAnnotation(updatedAnnotation) {
itemsMap[updatedAnnotation.id] = updatedAnnotation;
await chart.setProperties(updatedAnnotation);
}
async function toggleShowHideAnnotation(action) {
const toggleShowHideAnnotationId = "a4";
// Show / hide the annotation
chart[action](toggleShowHideAnnotationId, { animate: true, time: 280 });
// Add / remove the show glyph from the node subject
const nodeSubjectUpdate = {
id: "n4",
g: action === "hide" ? [showAnnotationGlyph] : undefined,
};
await setHoveredState(toggleShowHideAnnotationId, "0", false);
await chart.setProperties(nodeSubjectUpdate);
}
function isHoveringButton(id, subItem) {
const itemInteractionInfo = interactionInfo.items[id];
return (
itemInteractionInfo?.subItemType === subItem.type &&
itemInteractionInfo?.buttons[subItem.index]
);
}
// Enable cursor styling override while interacting with the chart
function setCursor(hovering) {
const type = hovering ? "pointer" : null;
const chartContainer = document.getElementById("klchart");
// get all non-cursor classes
const classes = Array.from(chartContainer.classList).filter(
(cn) => !cn.match(/^cursor-/)
);
if (type) {
classes.push(`cursor-${type}`);
}
chartContainer.className = classes.join(" ");
}
async function setHoveredState(id, subItem, hovering) {
if (hovering === undefined) {
hovering = isHoveringButton(id, subItem);
}
const itemIdToUpdate = hovering ? id : interactionInfo.lastHoveredButton?.id;
if (itemIdToUpdate) {
setCursor(hovering);
const itemInteractionInfo = interactionInfo.items[itemIdToUpdate];
let subItemIndexToUpdate = hovering
? subItem.index
: interactionInfo.lastHoveredButton.subItemIndex;
const itemToUpdate = chart.getItem(itemIdToUpdate);
const subItemPropName =
subItemTypePropMap[interactionInfo.items[itemIdToUpdate].subItemType];
const subItemArray = itemToUpdate[subItemPropName];
let styleName = null;
if (
subItemArray[subItemIndexToUpdate] &&
subItemArray[subItemIndexToUpdate].fbc !== colours.a5.selectedButton
) {
styleName = hovering ? "hovered" : "default";
}
// Remove the hover effect from the previously hovered theme button if we are travelling
// straight from the other theme button
else if (
interactionInfo.lastHoveredButton &&
subItemArray[interactionInfo.lastHoveredButton.subItemIndex]?.fbc !==
colours.a5.selectedButton
) {
subItemIndexToUpdate = interactionInfo.lastHoveredButton.subItemIndex;
styleName = "default";
}
if (styleName) {
const styleUpdate =
itemInteractionInfo.buttons[subItemIndexToUpdate]?.styles?.[styleName];
if (styleUpdate) {
subItemArray[subItemIndexToUpdate] = {
...subItemArray[subItemIndexToUpdate],
...styleUpdate,
};
await chart.setProperties({
id: itemIdToUpdate,
[subItemPropName]: subItemArray,
});
}
interactionInfo.lastHoveredButton = hovering
? {
id,
subItemIndex: subItem.index,
}
: null;
}
}
}
function initialiseInteractions() {
chart.on("click", async ({ id, subItem, preventDefault }) => {
if (itemsMap[id]) {
// Prevent item selection
preventDefault();
if (subItem.type === "label" || subItem.type === "glyph") {
// if the subItem is clickable trigger the appropriate onClick action
if (
interactionInfo.items[id] &&
interactionInfo.items[id].buttons[subItem.index]
) {
interactionInfo.items[id].buttons[subItem.index].onClick();
}
}
}
prettyPrint(itemsMap[id]);
});
chart.on(
"hover",
async ({ id, subItem }) => await setHoveredState(id, subItem)
);
}
async function startKeyLines() {
const options = {
handMode: true,
backColour: "rgb(230, 234, 224)",
navigation: false,
minZoom: 0.3,
hover: 10,
zoom: { adaptiveStyling: false },
iconFontFamily: "CI-Icons",
imageAlignment: {
[icons.x]: { e: 0.8 },
[icons.comment]: { e: 2 },
},
legacyGlyphAndLabelOrdering: false,
};
chart = await KeyLines.create({ container: "klchart", options });
await chart.load(data);
await chart.zoom("fit");
initialiseInteractions();
}
// if you need font icons, keep this.
// If not, call startKeyLines() from the listener on the last line and delete this function
function loadFonts() {
document.fonts.load('24px "CI-Icons"').then(startKeyLines);
}
window.addEventListener("DOMContentLoaded", loadFonts);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
<head>
<meta charset="UTF-8">
<title>Advanced Annotation Styles</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/ci-icons.css">
<link rel="stylesheet" type="text/css" href="/advancedannotationstyles.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="/advancedannotationstyles.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%;">Advanced Annotation Styles</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">
<div class="cicontent">
<p>Click any annotation to view its source code.</p>
<p>
Click the buttons on interactive annotations
to explore various user actions.
</p>
<pre id="display" style="-webkit-user-select: text; -khtml-user-select: text; -moz-user-select: text; -ms-user-select: text; word-break: keep-all;">Click an annotation</pre>
</div>
</form>
</div>
</div>
</div>
</div>
<div id="moreParent">
<div id="moreContainer">
</div>
</div>
<div id="lazyScripts">
</div>
</body>
</html>
/* Enable cursor overriding while hovering the chart */
.cursor-pointer canvas {
cursor: pointer !important;
}
Loading source
