//
//     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 &amp; 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 &amp; 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