//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//
//     Sample Code
//
//!    Create fully custom HTML overlays for the chart.
import KeyLines from "keylines";
import { data } from "./overlayhtml-data.js";

let chart;
const baseAnnotationOffset = 35;
const annotations = {};
const chartContainer = document.getElementById("klchart");
const templateHtml = document.getElementById("annotation_html").innerHTML;

function createAnnotation(item) {
  const itemId = item.id;
  // Add details to the template
  const html = templateHtml
    .replace(/{{annotation-id}}/, itemId)
    .replace(/{{close-id}}/, `${itemId}-close`)
    .replace(/{{note}}/, item.d.notes);

  // Add annotation to the DOM
  const annotationContainer = createAnnotationContainer(itemId);
  chartContainer.appendChild(annotationContainer);
  annotationContainer.innerHTML = html;

  // Update the annotations lookup state
  annotations[itemId] = {
    element: document.getElementById(`${itemId}`),
    show: true,
  };

  // Allow annotations to be closed manually
  document.getElementById(`${itemId}-close`).addEventListener("click", () => {
    closeAnnotation(itemId);
  });
}

function createAnnotationContainer(itemId) {
  const newAnnotationContainer = document.createElement("div");
  newAnnotationContainer.itemId = `annotation-container-${itemId}`;
  newAnnotationContainer.setAttribute("data-id", itemId);
  return newAnnotationContainer;
}

function updateAnnotationsPositions(zoomValue) {
  const zoom = zoomValue || chart.viewOptions().zoom;
  Object.keys(annotations).forEach((annotation) => {
    if (annotations[annotation].show) {
      const element = annotations[annotation].element;
      const item = chart.getItem(annotation);
      const point = chart.viewCoordinates(
        item.x + baseAnnotationOffset * (item.e || 1),
        item.y
      );
      // Translate the annotation so its position corresponds to the point of the arrow
      // and scale the size of the annotation depending on zoom level
      element.style.transform = `translate(5px, -44px) scale(${Math.max(
        0.9,
        Math.min(2, zoom)
      )}`;
      element.style.left = `${point.x}px`;
      element.style.top = `${point.y}px`;
    }
  });
}

function toggleStickyNoteGlyph(id, show) {
  chart.setProperties({
    id,
    g: show
      ? [
          {
            p: "ne",
            fi: {
              t: "fas fa-sticky-note",
              c: "#FEA95E",
            },
            e: 1.8,
          },
        ]
      : null,
  });
}

function toggleAllAnnotations(showAll) {
  Object.keys(annotations).forEach((itemId) => {
    if (showAll) {
      showAnnotation(itemId);
    } else {
      closeAnnotation(itemId);
    }
  });
}

function closeAnnotation(itemId) {
  const annotation = annotations[itemId];
  if (annotation.show) {
    annotation.show = false;
    toggleStickyNoteGlyph(itemId, true);
    annotation.element.style.visibility = "hidden";
  }
}

// Show the annotation and remove the indicative glyph
function showAnnotation(itemId) {
  const annotation = annotations[itemId];
  if (!annotation.show) {
    annotations[itemId].show = true;
    toggleStickyNoteGlyph(itemId, false);
    annotations[itemId].element.style.visibility = "visible";
  }
}

// Show the annotation of a selected item
function handleClick({ id }) {
  if (id && annotations[id] && !annotations[id].show) {
    showAnnotation(id);
    updateAnnotationsPositions();
  }
}

function initialiseInteractions() {
  document.getElementById("showAnnotations").addEventListener("click", () => {
    toggleAllAnnotations(true);
    updateAnnotationsPositions();
  });

  document.getElementById("hideAnnotations").addEventListener("click", () => {
    toggleAllAnnotations(false);
  });

  // Update all annotation positions on view change, but close them when the view dimensions change
  chart.on("view-change", () => {
    const { zoom } = chart.viewOptions();
    updateAnnotationsPositions(zoom);
  });

  chart.on("click", handleClick);

  // Keep the shown annotation position relative to the node
  // when dragging the node and panning the chart
  chart.on("drag-move", () => updateAnnotationsPositions());
}

async function startKeyLines() {
  const options = {
    handMode: true,
    iconFontFamily: "Font Awesome 5 Free",
    selectedNode: {
      fc: "#000000",
    },
  };

  chart = await KeyLines.create({ container: "klchart", options });

  chart.load(data);

  // If a node has a note, create an HTML annotation element for it
  chart.each({ type: "node" }, (item) => {
    if (item.d && item.d.notes) {
      createAnnotation(item);
    }
  });
  chart.layout("organic", { tightness: 3, consistent: true });
  initialiseInteractions();
}

function loadWebFonts() {
  document.fonts.load('24px "Font Awesome 5 Free"').then(startKeyLines);
}

window.addEventListener("DOMContentLoaded", loadWebFonts);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
  <head>
    <meta charset="UTF-8">
    <title>HTML Overlays</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="/css/fontawesome5/regular.css">
    <link rel="stylesheet" type="text/css" href="/overlayhtml.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="/overlayhtml.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%;">HTML Overlays</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">
              <p>Use the <i class="fa fa-times"></i> and <i class="fa fa-sticky-note" style="color: #FEA95E"></i> controls or the buttons below to toggle closing and opening the notes.</p>
              <p>
                Pan and zoom the chart to see how open notes respond to view changes.
                
              </p>
              <div class="cicontent">
                <fieldset>
                  <input class="btn btn-block" type="button" value="Open all sticky notes" id="showAnnotations">
                  <input class="btn btn-block" type="button" value="Close all sticky notes" id="hideAnnotations">
                </fieldset>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
    <div id="moreParent">
      <div id="moreContainer">
        <template id="annotation_html">
          <div class="annotation" id="{{annotation-id}}" style="margin:0px; transform-origin:left; position: absolute; min-width: 330px;">
            <div class="arrow"></div>
            <h2 class="annotation-title"><strong>Notes</strong>
              <button class="close-button" type="submit" id="{{close-id}}"><i class="fa fa-times"></i></button>
            </h2>
            <div class="annotation-content">{{note}}</div>
          </div>
        </template>
      </div>
    </div>
    <div id="lazyScripts">
    </div>
  </body>
</html>
.klchart {
  overflow: hidden;
}
  
/* Annotation styling */
.annotation {
  background: #ffffff;
  pointer-events: none;
}

.annotation-title {
  background-color: #FEA95E;
  border: 1px solid #808080;
  border-bottom: none;
  font-size: 14px;
  padding: 8px 14px;
  margin: 0px;
  line-height: 20px;
  top: 0px;
}

.annotation-content {
  border: 1px solid #808080;
  font-size: 12px;
  padding: 8px 14px;
}

.annotation .arrow {
  background: #ffffff;
  border-bottom: 1px solid #808080;
  border-left: 1px solid #808080;
  transform: translate(-5px, 39px) rotateZ(45deg);
  width: 10px;
  height: 10px;
  position: absolute;
}
  
.annotation .close-button {
  background-color: #FEA95E;
  height: 15px;
  width: 15px;
  padding: 0px 0px;
  line-height: 17px;
  float: right;
  pointer-events: all;
}

.annotation .fa-times {
  color: #000000;  
}
Loading source