//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
// Sample Code
//
//! Save a chart and then load it in a new session.
import KeyLines from "keylines";
import getData from "./saveload-data.js";
let chart;
// local Database: just a JS Map object in this demo
// but it could also be a real DB in the server using
// an AJAX call to send the data
const dbStore = new Map();
// We're using this variable to create progressive ids
// for saved charts
let counter = 1;
// This boolean variable is used to prevent savings during state transitions
let hasLoaded = true;
const saveButton = document.getElementById("save");
async function restoreChart(evt) {
// disable the button
saveButton.classList.add("disabled");
const previousSave = dbStore.get(evt.target.id);
// set the restore flag to 'Dirty'
hasLoaded = false;
// restore the data
await chart.load(previousSave);
// allow changes to be savable
hasLoaded = true;
}
async function saveChart() {
// serialize chart
const chartState = chart.serialize();
// If the demo is served from the file system the canvas cannot be exported as it is subject to
// CORS issues - see the Cross-Origin Images page in the Documentation section
try {
// save a small image thumbnail of the chart
const thumbnail = await chart.export({
type: "svg",
fitTo: { width: 348, height: 300 },
fonts: {
"Font Awesome 5 Free": {
src: "./fonts/fontAwesome5/fa-solid-900.woff",
},
},
});
// set a name for the save
const name = `Chart ${counter}`;
counter++;
// save the chart:
// in this demo we're saving the serialized chart in this local storage
// but it could also be sent with an AJAX call
dbStore.set(name, chartState);
// create a new entry in the list on the page
const savePreviewContainer = document
.getElementById("entryTemplate")
.cloneNode(true);
// set the image
const img = savePreviewContainer.querySelector("img");
img.src = thumbnail.url;
img.alt = name;
img.id = name;
// add click event
img.addEventListener("click", (event) => {
restoreChart(event);
});
const deleteImg = savePreviewContainer.querySelector("#delete");
deleteImg.addEventListener("click", () => {
dbStore.delete(name);
// The thumbnail image is retained by the browser so its very important
// to free it when it's no longer needed
URL.revokeObjectURL(thumbnail.url);
savePreviewContainer.remove();
});
const savedCharts = document.getElementById("savedCharts");
savedCharts.insertBefore(savePreviewContainer, savedCharts.firstChild);
// disable the button
saveButton.classList.add("disabled");
} catch (err) {
// show an error message about CORs issues
Array.from(document.getElementsByClassName("alert")).forEach((e) => {
e.style.display = "block";
});
}
}
function checkChanges() {
// don't catch events during the restore transition!
if (hasLoaded) {
// enable the save button
saveButton.classList.remove("disabled");
}
}
function getImageAlignment() {
// Images are added to to the imageAlignment object
// using their path as the key and an object describing the
// adjustments to their position and scale as the value.
const icons = {
"fas fa-network-wired": { e: 0.7 },
"fas fa-laptop-code": { e: 0.7 },
"fas fa-tablet-alt": { e: 0.7 },
"fas fa-server": { e: 0.7 },
};
const imageAlignment = {};
// get a list of the font icon class names
const iconNames = Object.keys(icons);
iconNames.forEach((icon) => {
// find the unicode value of each, and add to the imageAlignment object
imageAlignment[icon] = icons[icon];
});
return imageAlignment;
}
function doLayout() {
return chart.layout("organic", { tightness: 2 });
}
async function startKeyLines() {
const imageAlignment = getImageAlignment();
chart = await KeyLines.create({
container: "klchart",
options: {
linkStyle: { inline: true },
logo: "/images/Logo.png",
iconFontFamily: "Font Awesome 5 Free",
handMode: true,
// Font icons and images can often be poorly aligned.
// Set offsets to the icons to ensure they are centred correctly.
imageAlignment,
},
});
const data = await getData();
chart.load(data);
// these functions set up the drag over behaviour
chart.on("prechange", checkChanges);
chart.on("view-change", checkChanges);
// redo the layout
document.getElementById("layout").addEventListener("click", doLayout);
// save the chart: fire it only if not disabled!
saveButton.addEventListener("click", () => {
if (!saveButton.classList.contains("disabled")) {
saveChart();
}
});
// save the first checkpoint after we've finished the layout
await doLayout();
saveChart();
}
async function loadFontsAndStart() {
document.fonts.load('24px "Font Awesome 5 Free"').then(startKeyLines);
}
window.addEventListener("DOMContentLoaded", loadFontsAndStart);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
<head>
<meta charset="UTF-8">
<title>Save & Load Charts</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="/saveload.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="/saveload.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%;">Save & Load Charts</text>
</svg>
</div>
<div class="tab-content-panel" data-tab-group="rhs">
<div class="toggle-content is-visible tabcontent" id="controlsTab">
<p>How to save charts and reload them afterwards.</p>
<form autocomplete="off" onsubmit="return false" id="rhsForm">
<div class="cicontent flex-column flex1">
<p>Move things around in the chart and then click save.</p>
<div style="margin-top:20px;"></div>
<div class="form-stacked">
<input class="btn btn-spaced" type="button" value="Layout" id="layout">
<input class="btn btn-spaced" type="button" value="Save" id="save">
</div>
<fieldset class="flex-column flex1">
<legend>Saved Charts:</legend>
<p style="margin-top:10px;">Click on a chart in the list below to restore it.</p>
<ul class="thumbnails unstyled flex1" style="padding: 0px; scrollbar-width: none; overflow: auto;" id="savedCharts"></ul>
</fieldset>
</div>
</form>
</div>
</div>
</div>
</div>
<div id="moreParent">
<div id="moreContainer">
<div class="hidden">
<li class="savedChart" id="entryTemplate"><a id="delete"><i class="fa fa-times"></i></a><a class="thumbnail text-center small"><img class="saved"></a></li>
</div>
</div>
</div>
<div id="lazyScripts">
</div>
</body>
</html>
img.saved {
object-fit: contain;
}
.thumbnail {
background: white;
height: 100%;
padding: 0px;
}
#delete {
display: none;
color: #333;
position: absolute;
right: 0px;
top: 0px;
margin: 8px;
background: transparent;
}
.savedChart {
position: relative;
height: auto;
width: auto;
text-align: center;
width: max-content;
margin: 0 auto;
}
.savedChart:hover #delete {
display: block;
}
.thumbnails li {
margin-bottom: 16px;
}
.thumbnails li:last-child {
margin-bottom: 0px;
}
#controlsTab.is-visible, .is-visible #rhsForm {
display: flex;
flex: 1;
flex-direction: column;
}
Loading source
