//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
// Sample Code
//
//! Work with data spanning multiple levels.
import KeyLines from "keylines";
import data from "./sequential-data.js";
let chart;
let generatedData;
let maxLevels;
const options = {
multiComp: false,
};
const linkShapeInputs = [
...document.querySelectorAll('input[name="linkshape"]'),
];
const orientationInputs = [
...document.querySelectorAll('input[name="orientation"]'),
];
const generateDataEl = document.getElementById("generate");
const levelOrderingInputs = [
...document.querySelectorAll('input[name="levelOrdering"]'),
];
const orderByInputs = [...document.querySelectorAll('input[name="orderBy"]')];
const stretchSliderEl = document.getElementById("stretchSlider");
const stretchSliderInput = document.querySelector('input[name="stretch"]');
// Return a colour gradient scale by adjusting the alpha value based on the max levels
function getColourScale() {
const colourScale = [];
const startColour = { r: 0, g: 201, b: 128 };
const endColour = { r: 255, g: 255, b: 255 };
const deltaR = (endColour.r - startColour.r) / maxLevels;
const deltaG = (endColour.g - startColour.g) / maxLevels;
const deltaB = (endColour.b - startColour.b) / maxLevels;
colourScale.push(`rgb(${startColour.r}, ${startColour.g}, ${startColour.b})`);
for (let i = 1; i <= maxLevels; i++) {
colourScale.push(
`rgb(${Math.round(startColour.r + deltaR * i)}, ${Math.round(
startColour.g + deltaG * i
)}, ${Math.round(startColour.b + deltaB * i)})`
);
}
return colourScale;
}
function resetLevelColourScale(isUniform) {
const colourScale = getColourScale();
const props = [];
chart.each({ type: "node" }, ({ id, d }) =>
props.push(
isUniform
? // Revert to the same colour for all nodes
{ id, c: "rgb(0, 201, 128)", fc: "rgba(0,0,0,0)" }
: // Set the colour scale on the level assignment
{ id, c: colourScale[d.level], fc: "rgb(0,0,0)" }
)
);
chart.setProperties(props);
}
function resetGlyphs(show) {
// rescale number glyphs to distinguish them in the UI from level numbers
const scale = 10;
const props = [];
chart.each({ type: "node" }, ({ id, d }) => {
const g =
show && d.col !== void 0
? [
{
id,
t: d.col * scale,
c: "rgb(128, 0, 0)",
p: 45,
r: 40,
e: 3,
b: null,
},
]
: null;
props.push({ id, g });
});
chart.setProperties(props);
}
// Runs a sequential layout from the selected layout options
function runLayout() {
// Collect the layout options selected
const selectedNodes = chart
.selection()
.filter((item) => chart.getItem(item).type === "node");
const orientation = document.querySelector(
'input[name="orientation"]:checked'
).value;
const stretch = document.querySelector('input[name="stretch"]').value;
const linkShape = document.querySelector('input[name="linkshape"]:checked')
.value;
const layoutOpts = { stretch, orientation, linkShape };
// Get the current level ordering criteria
const orderMethod = document.querySelector(
'input[name="levelOrdering"]:checked'
).value;
if (orderMethod === "levels") {
// A colour gradient is used to highlight the ordering
resetLevelColourScale(false);
// The level value is used to assign levels
layoutOpts.level = "level";
} else {
resetLevelColourScale(true);
if (orderMethod === "top" && selectedNodes.length > 0) {
// Selected nodes from the chart will be placed at the top of the hierarchy
layoutOpts.top = selectedNodes;
}
// The default is auto levels are used to assign levels
}
// always show orderBy glyphs
resetGlyphs(true);
// Get the current column ordering criteria
const orderBy = document.querySelector('input[name="orderBy"]:checked');
layoutOpts.orderBy = {
property: orderBy.getAttribute("property"),
sortBy: orderBy.getAttribute("sortBy"),
};
chart.layout("sequential", layoutOpts);
}
function generateChartData() {
// Decide how many components we want
// Bias towards just a single component and occasionally leave it to fate
// Always display a single component on load
if (options.multiComp && Math.random() > 0.6) {
options.comps = Math.ceil(Math.random() * 3);
} else {
options.comps = 1;
}
generatedData = data.generate(options);
// Get the max number of levels generated
maxLevels = generatedData.chartLevel;
chart.load(generatedData.chartData);
options.multiComp = true;
runLayout();
}
function marqueeDragStartHandler({ type }) {
if (type === "marquee") {
chart.off("selection-change", runLayout);
}
}
function marqueeDragEndHandler({ type }) {
if (type === "marquee") {
chart.on("selection-change", runLayout);
runLayout();
}
}
function levelOrderingHandler(e) {
if (e.target.value === "top") {
chart.on("drag-start", marqueeDragStartHandler);
chart.on("drag-end", marqueeDragEndHandler);
chart.on("selection-change", runLayout);
} else {
chart.off("drag-start", marqueeDragStartHandler);
chart.off("drag-end", marqueeDragEndHandler);
chart.off("selection-change", runLayout);
}
runLayout();
}
function addEventHandlers() {
chart.on("drag-start", ({ type, setDragOptions }) => {
if (type === "node") {
setDragOptions({ type: "pan" });
}
});
// Generate a new chart
generateDataEl.addEventListener("click", generateChartData);
// Change link style
linkShapeInputs.forEach((input) => {
input.addEventListener("change", () => {
runLayout();
});
});
stretchSliderInput.addEventListener("change", () => {
// Update the slider value on the UI
const sliderValue = stretchSliderEl.value;
document.getElementById("stretchVal").innerHTML = ` ${sliderValue}`;
runLayout();
});
orientationInputs.forEach((input) => {
input.addEventListener("change", () => {
runLayout();
});
});
levelOrderingInputs.forEach((input) => {
input.addEventListener("change", levelOrderingHandler);
});
orderByInputs.forEach((input) => {
input.addEventListener("change", runLayout);
});
}
async function startKeyLines() {
const chartOptions = {
logo: { u: "/images/Logo.png" },
handMode: true,
selectedNode: {
ha0: {
c: "rgb(255, 156, 161)",
w: 10,
r: 40,
},
},
};
chart = await KeyLines.create({
container: "klchart",
options: chartOptions,
});
addEventHandlers();
generateChartData();
}
window.addEventListener("DOMContentLoaded", startKeyLines);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
<head>
<meta charset="UTF-8">
<title>Display Hierarchies</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">
<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="/sequential.js" crossorigin="use-credentials" 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%;">Display Hierarchies</text>
</svg>
</div>
<div class="tab-content-panel" data-tab-group="rhs">
<div class="toggle-content is-visible tabcontent" id="controlsTab">
<p>Use the sequential layout to display ordered hierarchies and customise it with various options.</p>
<form autocomplete="off" onsubmit="return false" id="rhsForm">
<div class="cicontent">
<input class="btn btn-kl" type="button" value="Generate" style="margin: 3px 0px 5px;" id="generate">
<fieldset>
<legend>Layout Options</legend>
<h5 style="margin-top:1px">Order of levels</h5>
<radio class="inline">
<input type="radio" name="levelOrdering" value="auto" checked="checked"><span> Auto</span>
</radio>
<radio class="inline">
<input type="radio" name="levelOrdering" value="top"><span> Selected nodes first</span>
</radio>
<radio class="inline">
<input type="radio" name="levelOrdering" value="levels"><span> Predefined</span>
</radio>
<h5 style="margin-top:10px">Order within levels</h5>
<radio class="inline">
<input type="radio" name="orderBy" value="auto" checked="checked"><span> Auto</span>
</radio>
<radio class="inline">
<input type="radio" name="orderBy" property="col" sortBy="descending"><span> Descending</span>
</radio>
<radio class="inline">
<input type="radio" name="orderBy" property="col" sortBy="ascending"><span> Ascending</span>
</radio>
<h5>Stretch distance between levels: <span style="font-weight: normal" id="stretchVal">1.5</span></h5>
<input id="stretchSlider" type="range" name="stretch" min="1" max="4" value="1.5" step="0.1">
<h5>Orientation</h5>
<radio class="inline">
<input type="radio" name="orientation" value="down" checked="checked"><span>Down</span>
</radio>
<radio class="inline">
<input type="radio" name="orientation" value="up"><span>Up</span>
</radio>
<radio class="inline">
<input type="radio" name="orientation" value="left"><span>Left</span>
</radio>
<radio class="inline">
<input type="radio" name="orientation" value="right"><span>Right</span>
</radio>
<h5>Link Shape</h5>
<radio class="inline">
<input type="radio" name="linkshape" value="curved" checked="checked"><span>Curved</span>
</radio>
<radio class="inline">
<input type="radio" name="linkshape" value="direct"><span>Direct</span>
</radio>
</fieldset>
</div>
</form>
</div>
</div>
</div>
</div>
<div id="moreParent">
<div id="moreContainer">
</div>
</div>
<div id="lazyScripts">
</div>
</body>
</html>
Loading source
