//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
// Sample Code
//
//! Create nodes with custom shapes, multiple labels and advanced styles.
import {
data,
invertedInteractiveNode,
normalInteractiveNode,
interactionStylePresets,
} from "./advancednodestyles-data.js";
import KeyLines from "keylines";
let chart;
// keeps track of the current state/styling of items on the chart
const itemsState = [...data.items];
function highlightSelection(id) {
// if the user hasn't clicked an item resize back to fit
if (!id) {
chart.foreground(() => true);
return;
}
// if there is a clicked item set it to be foregrounded
const clickedItem = chart.getItem(id);
if (clickedItem) {
chart.foreground((item) => clickedItem.id === item.id);
}
}
function formatPrintString(key, value) {
if (key === "x" || key === "y" || key === "bg") {
return undefined;
}
// prints the borderRadius array as a single value if all directions are set to the same value
if (key === "borderRadius") {
const set = new Set(value);
if (set.size === 1) {
return value[0];
}
}
return value;
}
// Formats the code snippets neatly in the display box on the right-hand side
function prettyPrint(id) {
const item = chart.getItem(id);
if (item) {
const json = JSON.stringify(item, formatPrintString, 1);
document.getElementById("display").innerHTML = json;
} else {
document.getElementById("display").innerHTML = "No node selected";
}
}
// Called when KeyLines is created
function initializeInteractions() {
// Zoom the view to its original position when double-clicking the background
chart.on("double-click", ({ id, preventDefault }) => {
if (!chart.getItem(id)) {
chart.zoom("fit", { animate: true, time: 500 });
}
// Default behaviour is to zoom in - we preventDefault to override this
preventDefault();
});
let interactiveNodesState = {};
const labelButtons = {};
const buttonGroupLabels = {};
function registerLabelButton(
itemId,
labelIndex,
onItemInteractionLabelStyle,
onClick
) {
labelButtons[itemId] = labelButtons[itemId] || {};
labelButtons[itemId][labelIndex] = { onClick };
labelButtons[itemId][
onItemInteractionLabelStyle
] = onItemInteractionLabelStyle;
}
function registerLabelButtonGroup(
itemId,
labelIndexArray,
onItemInteractionLabelStyle,
onClick
) {
for (const labelIndex of labelIndexArray) {
registerLabelButton(
itemId,
labelIndex,
onItemInteractionLabelStyle,
onClick
);
buttonGroupLabels[itemId] = {
...buttonGroupLabels[itemId],
[labelIndex]: labelIndexArray,
};
}
}
// Highlight and pretty print when a selection is made or if we click an interactive label
chart.on("click", ({ id, subItem, preventDefault }) => {
prettyPrint(id);
highlightSelection(id);
if (subItem.type === "label") {
const labelIndex = subItem.index;
if (labelButtons[id] && labelButtons[id][labelIndex]) {
labelButtons[id][labelIndex].onClick();
prettyPrint(id);
}
}
});
let hoveredButton = null;
chart.on("hover", async ({ id, subItem }) => {
const style = {};
const labelIndex = subItem?.type === "label" ? subItem.index : -1;
const isHoverableLabel =
id !== null && labelButtons[id] && labelButtons[id][labelIndex];
const isAButtonGroup =
isHoverableLabel && !!buttonGroupLabels?.[id]?.[labelIndex];
const hoverStyle = isHoverableLabel && labelButtons[id][style];
const sourceItem = itemsState.find((item) => item.id === id);
const chartElement = document.querySelector("#klchart canvas[style]");
// Always clear any hovered button first
if (hoveredButton) {
const hoveredButtonSourceItem = itemsState.find(
(item) => item.id === hoveredButton
);
await chart.setProperties({
id: hoveredButton,
t: hoveredButtonSourceItem?.t,
});
hoveredButton = null;
}
// clear cursor style
chartElement.classList.remove("cursor");
if (isHoverableLabel && !isAButtonGroup) {
const label = [...sourceItem.t];
label[labelIndex] = {
...label[labelIndex],
...hoverStyle,
};
await chart.setProperties({ id, t: label });
hoveredButton = id;
chartElement.classList.toggle("cursor");
} else if (isAButtonGroup) {
const label = [...sourceItem.t];
for (const index of buttonGroupLabels[id][labelIndex]) {
label[index] = {
...label[index],
...hoverStyle,
};
}
await chart.setProperties({ id, t: label });
hoveredButton = id;
chartElement.classList.toggle("cursor");
}
});
async function toggleNodeStyle(itemId) {
const interactiveNodeState = interactiveNodesState[itemId];
const nodeStyle = !interactiveNodeState
? invertedInteractiveNode
: normalInteractiveNode;
// remove x and y from the styling to prevent the node snapping back to it's original position
delete nodeStyle.x;
delete nodeStyle.y;
// updtate the item state with the new toggled style
const itemIndex = itemsState.findIndex((item) => item.id === itemId);
itemsState[itemIndex] = nodeStyle;
await chart.setProperties({ id: itemId, ...nodeStyle });
interactiveNodesState[itemId] = !interactiveNodesState[itemId];
}
async function toggleLabelButtons(itemId, labelIndex, style) {
// track which buttons are toggled
const currentNodeInteractiveState = interactiveNodesState[itemId] ?? [];
currentNodeInteractiveState[labelIndex] =
currentNodeInteractiveState[labelIndex] === undefined
? true
: !currentNodeInteractiveState[labelIndex];
interactiveNodesState[itemId] = currentNodeInteractiveState;
// make sure all buttons are styled correctly depending on their toggle state
const currentStyle = itemsState.find((item) => item.id === itemId);
const originalStyle = data.items.find((item) => item.id === itemId);
const toggleStyle = { ...currentStyle, t: [...currentStyle.t] };
toggleStyle.t[labelIndex] = {
...toggleStyle.t[labelIndex],
...style,
};
// remove x and y from the styling to prevent the node snapping back to it's original position
delete toggleStyle.x;
delete toggleStyle.y;
// update the item state with the new toggled style
const itemIndex = itemsState.findIndex((item) => item.id === itemId);
itemsState[itemIndex] = toggleStyle;
currentNodeInteractiveState.forEach((toggleValue, index) => {
const originalLabelStyle = { ...originalStyle.t[index] };
const labelStyle = toggleValue ? style : originalLabelStyle;
toggleStyle.t[index] = {
...toggleStyle.t[index],
...labelStyle,
};
});
await chart.setProperties({ id: itemId, ...toggleStyle });
}
registerLabelButton(
"t2",
2,
interactionStylePresets.interactiveNodeHover,
() => {
toggleNodeStyle("t2");
}
);
registerLabelButtonGroup(
"t2",
[3, 4],
interactionStylePresets.interactiveNodeHover,
() => window.open("/stylingnodes.htm#advancedlabelstyling", "_blank")
);
registerLabelButton(
"outside-buttons-rectangle",
3,
interactionStylePresets.outsideLabelActive,
() => {
toggleLabelButtons(
"outside-buttons-rectangle",
3,
interactionStylePresets.outsideLabelHover
);
}
);
registerLabelButton(
"outside-buttons-rectangle",
5,
interactionStylePresets.outsideLabelActive,
() => {
toggleLabelButtons(
"outside-buttons-rectangle",
5,
interactionStylePresets.outsideLabelHover
);
}
);
registerLabelButton(
"outside-buttons-rectangle",
7,
interactionStylePresets.outsideLabelActive,
() => {
toggleLabelButtons(
"outside-buttons-rectangle",
7,
interactionStylePresets.outsideLabelHover
);
}
);
registerLabelButton(
"outside-buttons-circle",
3,
interactionStylePresets.outsideLabelActive,
() => {
toggleLabelButtons(
"outside-buttons-circle",
3,
interactionStylePresets.outsideLabelHover
);
}
);
registerLabelButton(
"outside-buttons-circle",
4,
interactionStylePresets.outsideLabelActive,
() => {
toggleLabelButtons(
"outside-buttons-circle",
4,
interactionStylePresets.outsideLabelHover
);
}
);
registerLabelButton(
"outside-buttons-circle",
5,
interactionStylePresets.outsideLabelActive,
() => {
toggleLabelButtons(
"outside-buttons-circle",
5,
interactionStylePresets.outsideLabelHover
);
}
);
}
async function startKeyLines() {
const options = {
logo: { u: "/images/Logo.png" },
handMode: true,
selectionColour: "#FF9933",
iconFontFamily: "Font Awesome 5 Free",
overview: { icon: false },
navigation: { shown: false },
hover: 0,
};
chart = await KeyLines.create({ container: "klchart", options });
await chart.load(data);
await chart.zoom("fit");
initializeInteractions();
}
function loadFontsAndStart() {
document.fonts.ready
.then(() =>
Promise.all([
document.fonts.load('1em "Font Awesome 5 Free"'),
document.fonts.load('1em "Raleway"'),
document.fonts.load('1em "Montserrat"'),
document.fonts.load('1em "Varela Round"'),
document.fonts.load('1em "Material Icons"'),
])
)
.then(startKeyLines);
}
window.addEventListener("DOMContentLoaded", loadFontsAndStart);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
<head>
<meta charset="UTF-8">
<title>Advanced Node 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/fontawesome5/fontawesome.css">
<link rel="stylesheet" type="text/css" href="/css/fontawesome5/solid.css">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Raleway&family=Montserrat&family=Varela+Round">
<link rel="stylesheet" type="text/css" href="/advancednodestyles.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="/advancednodestyles.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 Node Styles</text>
</svg>
</div>
<div class="tab-content-panel" data-tab-group="rhs">
<div class="toggle-content is-visible tabcontent" id="controlsTab">
<p>Select a node to view its source code and interact with the buttons on the Interactive Node.</p>
<form autocomplete="off" onsubmit="return false" id="rhsForm">
<div class="cicontent">
<fieldset>
<pre id="display" style="-webkit-user-select: text; -khtml-user-select: text; -moz-user-select: text; -ms-user-select: text; word-break: keep-all;font-family: monospace, "Material Icons"">No node selected</pre>
</fieldset>
</div>
</form>
</div>
</div>
</div>
</div>
<div id="moreParent">
<div id="moreContainer">
</div>
</div>
<div id="lazyScripts">
</div>
</body>
</html>
@import url("https://fonts.googleapis.com/icon?family=Material+Icons");
/* Enable cursor overriding while hovering the chart */
.cursor {
cursor: pointer !important;
}
Loading source
