Search

HTML Overlays

Styling

Create fully custom HTML overlays for the chart.

HTML Overlays
View live example →

You can create your own HTML overlay to add elements made from scratch to your chart.

With KeyLines events, you can integrate these elements into the chart and make them respond to chart changes, view changes and user interactions.

The view-change event lets you scale the open notes with the chart when the view changes. In this case, the note size is also capped as the chart is zoomed in to keep the chart balanced and clear.

The drag-move event keeps the note position relative to the node when dragging the node and panning the chart.

Key functions used:

import KeyLines from "keylines";
import { data } from "./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);
import KeyLines from "keylines";
export const data = {
  type: "LinkChart",
  items: [
    {
      type: "node",
      id: "node1",
      t: "Harry Barnes",
      d: {
        notes: "Prior conviction for financial crime in 2017.",
      },
      fi: {
        t: "far fa-user",
        c: "#397af2",
      },
      tc: false,
      fc: "#666a6d",
      fb: true,
      fs: 12,
    },
    {
      type: "node",
      id: "node2",
      t: "Contract 202005-ZA",
      fi: {
        t: "far fa-handshake",
        c: "#000000",
      },
      tc: false,
      fc: "#666a6d",
      fb: true,
      b: "#000000",
      bw: 1,
      fs: 12,
    },
    {
      type: "node",
      id: "node9",
      t: "Contract 201905-GB",
      fi: {
        t: "far fa-handshake",
        c: "#000000",
      },
      tc: false,
      fc: "#666a6d",
      fb: true,
      b: "#000000",
      bw: 1,
      fs: 12,
    },
    {
      type: "node",
      id: "node3",
      t: "Brooke Webb",
      fi: {
        t: "far fa-user",
        c: "#397af2",
      },
      tc: false,
      fc: "#666a6d",
      fb: true,
      fs: 12,
    },
    {
      type: "node",
      id: "node4",
      t: "ABC Ltd",
      fi: {
        t: "far fa-building",
        c: "#000000",
      },
      tc: false,
      fc: "#666a6d",
      fb: true,
      b: "#000000",
      bw: 1,
      fs: 12,
    },
    {
      type: "node",
      id: "node5",
      t: "Rory Hopkins",
      fi: {
        t: "far fa-user",
        c: "#397af2",
      },
      tc: false,
      fc: "#666a6d",
      fb: true,
      fs: 12,
    },
    {
      type: "node",
      id: "node6",
      t: "Lynn Young",
      d: {
        notes: "Currently under investigation for contract 201904-JS.",
      },
      fi: {
        t: "far fa-user",
        c: "#397af2",
      },
      tc: false,
      fc: "#666a6d",
      fb: true,
      fs: 12,
    },
    {
      type: "node",
      id: "node7",
      t: "XYZ Ltd",
      fi: {
        t: "far fa-building",
        c: "#000000",
      },
      tc: false,
      fc: "#666a6d",
      fb: true,
      b: "#000000",
      bw: 1,
      fs: 12,
    },
    {
      type: "link",
      id: "node1-node4",
      id1: "node1",
      id2: "node4",
    },
    {
      type: "link",
      id: "node4-node3",
      id1: "node4",
      id2: "node3",
    },
    {
      type: "link",
      id: "node4-node2",
      id1: "node4",
      id2: "node2",
    },
    {
      type: "link",
      id: "node7-node5",
      id1: "node5",
      id2: "node7",
    },
    {
      type: "link",
      id: "node2-node6",
      id1: "node2",
      id2: "node6",
    },
    {
      type: "link",
      id: "node2-node7",
      id1: "node2",
      id2: "node7",
    },
    {
      type: "link",
      id: "node6-node9",
      id1: "node6",
      id2: "node9",
    },
    {
      type: "link",
      id: "node3-node9",
      id1: "node3",
      id2: "node9",
    },
  ],
};
<!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="@ci/theme/kl/css/keylines.css" />
    <link rel="stylesheet" type="text/css" href="@ci/theme/kl/css/minimalsdk.css" />
    <link rel="stylesheet" type="text/css" href="@ci/theme/kl/css/sdk-layout.css" />
    <link rel="stylesheet" type="text/css" href="@ci/theme/kl/css/demo.css" />
    <link
      rel="stylesheet"
      type="text/css"
      href="@fortawesome/[email protected]/css/fontawesome.css"
    />
    <link
      rel="stylesheet"
      type="text/css"
      href="@fortawesome/[email protected]/css/solid.css"
    />
    <link
      rel="stylesheet"
      type="text/css"
      href="@fortawesome/[email protected]/css/regular.css"
    />
    <link rel="stylesheet" href="./style.css" />
  </head>
  <body>
    <div id="klchart" class="klchart"></div>
    <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
    >
    <script type="module" src="./code.js"></script>
  </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;
}

Terms of use

These terms do not alter or supersede any existing agreements between you (or your employer) and us.

By accessing or using any Content you agree to be bound by these Terms of Use. Please review these terms carefully before using the website.

The contents of this website, including but not limited to any text, code samples, API references, schemas, interactive tools, and other materials (collectively, the 'Content'), are made available for informational and internal evaluation purposes only. All intellectual property rights in the Content are reserved. No licence is granted to use the Content for any commercial purpose, or to copy, distribute, modify, reverse-engineer, or incorporate any part of the Content into any product or service, without our prior written consent.

This Content is provided “as is” and “as available,” without any representations, warranties, or guarantees of any kind, whether express or implied, including but not limited to implied warranties of merchantability, fitness for a particular purpose, non-infringement, or accuracy. To the fullest extent permitted by applicable law, we expressly exclude and disclaim all implied warranties, conditions, and other terms that might otherwise be implied.

We disclaim all liability for any loss or damage, whether direct, indirect, incidental, consequential, or otherwise, arising from any reliance placed on the Content or from your use of it, to the fullest extent permitted by applicable law. By continuing to access or use the Content, you acknowledge and agree to these terms.