Search

Interaction Events

Interactions

Interact with the chart to explore keyboard, mouse and touch events.

Interaction Events
View live example →

KeyLines events are flexible and customisable and support mouse, keyboard and touch device inputs.

In this demo you can experiment with chart events and common user gestures such as clicking or dragging on a variety of chart items.

While you interact, the events are added to the top of the Event list as they are fired.

The Event details window shows the object passed to the event handler of the active event.

See also

Demos: Time Bar Events

Key functions used:

import KeyLines from "keylines";
import { data } from "./data.js";

let chart;

const events = [
  "click",
  "context-menu",
  "double-click",
  "drag-end",
  "drag-move",
  "drag-over",
  "drag-start",
  "hover",
  "key-down",
  "key-up",
  "pointer-down",
  "pointer-move",
  "pointer-up",
  "prechange",
  "selection-change",
  "view-change",
  "wheel",
];

const ignoreList = ["drag-move", "pointer-move"];
let eventDetails = {};
let eventCounter = 0;

function positionModifierKeys() {
  const chartDivRect = document.getElementById("klchart").getBoundingClientRect();
  const modifierKeysDiv = document.getElementById("modifier-keys");
  modifierKeysDiv.style.top = `${chartDivRect.bottom - modifierKeysDiv.offsetHeight}px`;
  modifierKeysDiv.style.left = `${chartDivRect.left}px`;
  modifierKeysDiv.style.opacity = 1;
}

function initialiseInteractions() {
  // Create a map of which events should be listened to
  const onEvents = new Map(
    events.filter((name) => ignoreList.indexOf(name) === -1).map((event) => [event, true])
  );

  // Get all the elements we wish to be able to modify
  const eventsButton = document.getElementById("filterEvents");
  const eventsContainer = document.getElementById("eventsListContainer");
  const eventsList = document.getElementById("eventsList");
  const noEvents = document.getElementById("noEvents");
  const allEvents = document.getElementById("allEvents");
  const eventNameDisplay = document.getElementById("eventName");
  const eventDetailsPre = document.getElementById("eventDetails");
  const prevEventButton = document.getElementById("prevEvent");
  const nextEventButton = document.getElementById("nextEvent");
  const eventLog = document.getElementById("eventLog");
  const eventsTable = document.getElementById("events");
  const clearButton = document.getElementById("clear");
  // Get the bounds of the event log so can ensure the highlighted event is in view
  const { top: eventLogTop, bottom: eventLogBottom } = eventLog.getBoundingClientRect();

  function displayEvent(evtId) {
    const details = eventDetails[evtId];
    const json = JSON.stringify(details.event, undefined, 2);
    eventDetailsPre.innerHTML = json;
    eventDetailsPre.dataset.id = evtId;
    eventNameDisplay.innerHTML = `: '${details.name}'`;

    // Disable the previous / next event buttons if no such event exists
    prevEventButton.disabled = !eventDetails[evtId - 1];
    nextEventButton.disabled = !eventDetails[evtId + 1];
  }

  function highlightRow(eventRow) {
    [...document.getElementsByClassName("selectedEvent")].forEach((el) => {
      el.classList.remove("selectedEvent");
    });
    eventRow.classList.add("selectedEvent");
    // Check if row is visible, if not scroll into view
    const distToTop = eventRow.getBoundingClientRect().top - eventLogTop;
    if (distToTop < 0) {
      eventLog.scrollTop += distToTop;
    } else {
      const distToBottom = eventRow.getBoundingClientRect().bottom - eventLogBottom;
      if (distToBottom > 0) {
        eventLog.scrollTop += distToBottom;
      }
    }
  }

  function onRowClick(rowClickEvt) {
    const row = rowClickEvt.currentTarget;
    highlightRow(row);
    displayEvent(parseInt(row.dataset.lastId, 10));
  }

  function displayNextEvent() {
    // Get the ID of the next event
    const evtId = parseInt(eventDetailsPre.dataset.id, 10) + 1;
    // Check if it is grouped in the currently highlighted row
    const currEvtRow = document.getElementsByClassName("selectedEvent")[0];
    if (parseInt(currEvtRow.dataset.lastId, 10) < evtId) {
      highlightRow(currEvtRow.previousSibling);
    }
    // Display the details of this event
    displayEvent(evtId);
  }

  function displayPrevEvent() {
    // Get the ID of the previous event
    const evtId = parseInt(eventDetailsPre.dataset.id, 10) - 1;
    // Check if it is grouped in the currently highlighted row
    const currEvtRow = document.getElementsByClassName("selectedEvent")[0];
    if (parseInt(currEvtRow.dataset.firstId, 10) > evtId) {
      highlightRow(currEvtRow.nextSibling);
    }
    // Display the details of this event
    displayEvent(evtId);
  }

  // Add the HTML for the event filter
  events.forEach((event) => {
    // Create a copy of the template
    const eventLabel = document.createElement("label");
    eventLabel.innerHTML = `<input type="checkbox" id="${event}" ${
      onEvents.has(event) ? 'checked="checked"' : ""
    }"> ${event}`;
    const inp = eventLabel.querySelector("input");
    // Add the on change listener
    eventLabel.addEventListener("change", () => {
      onEvents.set(event, inp.checked);
    });

    // Make the no events button disable this input
    noEvents.addEventListener("click", () => {
      inp.checked = false;
      onEvents.set(event, false);
    });

    // Make the all events button enable this input
    allEvents.addEventListener("click", () => {
      inp.checked = true;
      onEvents.set(event, true);
    });

    // Add it to the event list
    eventsList.append(eventLabel);
  });

  clearButton.addEventListener("click", () => {
    clearButton.classList.add("hide");
    eventsTable.innerHTML = "";
    eventNameDisplay.innerHTML = "";
    eventDetailsPre.innerHTML = "No event selected";
    eventDetailsPre.dataset.id = undefined;
    prevEventButton.disabled = true;
    nextEventButton.disabled = true;
    eventDetails = {};
  });

  eventsButton.addEventListener("click", (event) => {
    const classList = eventsContainer.classList;
    if (classList.contains("hide")) {
      classList.remove("hide");
    } else {
      classList.add("hide");
    }
    event.stopPropagation();
  });

  document.addEventListener("click", (event) => {
    if (event.target.closest("#eventsListContainer") === null) {
      eventsContainer.classList.add("hide");
    }
  });

  nextEventButton.addEventListener("click", displayNextEvent);
  prevEventButton.addEventListener("click", displayPrevEvent);

  chart.on("click", ({ id }) => {
    if (id === "7") throw new Error("Example Error Event");
  });

  // Attach an event handler to all the chart events
  chart.on("all", (evt) => {
    // Update the active modifier keys
    if (evt.event && evt.event.modifierKeys) {
      Object.keys(evt.event.modifierKeys).forEach((key) => {
        if (evt.event.modifierKeys[key]) {
          document.getElementById(key).classList.add("active");
        } else {
          document.getElementById(key).classList.remove("active");
        }
      });
    }
    if (onEvents.get(evt.name)) {
      // Add this event to the log
      const eventObject = {};
      if (evt.event) {
        Object.keys(evt.event).forEach((prop) => {
          if (prop !== "defaultPrevented") eventObject[prop] = evt.event[prop];
        });
      }
      const lastEvt = eventDetails[eventCounter++];
      eventDetails[eventCounter] = { name: evt.name, event: eventObject };
      // Check if a repeat of the previous event
      if (
        eventObject &&
        lastEvt &&
        evt.name === lastEvt.name &&
        eventObject.id === lastEvt.event.id
      ) {
        // Group it with the previous event and up the counter for that event
        const td = document.getElementsByClassName("eventname")[0];
        const rowData = td.parentElement.dataset;
        const countSpan = td.children[0];
        countSpan.innerText = `(${eventCounter - rowData.firstId + 1})`;
        rowData.lastId = eventCounter;
      } else {
        // Add it to the top of the event log
        const tr = document.createElement("tr");
        tr.innerHTML = `<td id='${eventCounter}' class='eventname'>'${evt.name}'<span></span></td>`;
        tr.dataset.firstId = eventCounter;
        tr.dataset.lastId = eventCounter;
        tr.onclick = onRowClick;
        eventsTable.insertBefore(tr, eventsTable.firstChild);
      }
      // Ensure the top row is highlighed
      highlightRow(document.getElementsByClassName("eventname")[0].parentElement);
      // Update the displayed event details to show this event
      displayEvent(eventCounter);

      // Unhide the clear button
      clearButton.classList.remove("hide");
    }
  });

  // Hide the modifier keys when open the source tab
  [...document.getElementsByClassName("tablinks")].forEach((el) => {
    el.addEventListener("click", () => {
      document.getElementById("modifier-keys").style.opacity = el.name === "sourceTab" ? 0 : 1;
    });
  });
}

async function startKeyLines() {
  // Create the keylines chart
  chart = await KeyLines.create({
    container: "klchart",
    options: {
      logo: { u: "/images/Logo.png" },
      iconFontFamily: "Font Awesome 5 Free",
      selectedNode: { b: "#72B300" },
    },
  });
  chart.load(data);
  chart.zoom("fit");

  // Position the modifier keys overlay on the chart
  positionModifierKeys();
  window.addEventListener("resize", positionModifierKeys);
  // Deactivate them when the chart isn't focussed
  document.getElementById("klchart").addEventListener("focusout", () => {
    [...document.getElementsByClassName("key")].forEach((el) => el.classList.remove("active"));
  });

  initialiseInteractions();
}

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

window.addEventListener("DOMContentLoaded", loadFontsAndStart);
import KeyLines from "keylines";
export const data = {
  type: "LinkChart",
  items: [
    {
      id: "an1",
      type: "annotation",
      subject: "1",
      t: { t: "A simple annotation" },
      position: { distance: 50, angle: "ne" },
    },
    {
      id: "1",
      type: "node",
      c: "#3485e7",
      x: 80,
      y: 80,
      g: [{ p: "ne", c: "rgb(255, 175, 0)", t: "43" }],
    },
    {
      id: "2",
      type: "node",
      c: "#3485e7",
      x: 140,
      y: 350,
      bu: { p: "nw", c: "#e0e0e0", t: "Message" },
    },
    {
      id: "3",
      type: "node",
      c: "#3485e7",
      x: 350,
      y: 400,
      donut: { v: [5, 3, 2, 4, 1] },
    },
    {
      id: "4",
      type: "node",
      c: "#3485e7",
      x: 500,
      y: 450,
      g: [{ p: 45, c: "#c41f30" }],
    },
    {
      id: "5",
      type: "node",
      c: "#3485e7",
      x: 600,
      y: 250,
    },
    {
      id: "6",
      type: "node",
      c: "#3485e7",
      x: 200,
      y: 200,
      ha0: { r: 40, w: 15, c: "rgba(180,180,180,0.5)" },
      ha1: { r: 60, w: 15, c: "rgba(200,200,200,0.5)" },
      ha2: { r: 80, w: 15, c: "rgba(220,220,220,0.5)" },
    },
    {
      id: "7",
      type: "node",
      c: "#3485e7",
      t: "Throws error on click",
      x: 500,
      y: 150,
    },
    {
      id: "8",
      type: "node",
      c: "#3485e7",
      w: "auto",
      h: "auto",
      fbc: "rgba(0, 0, 0, 0)",
      fc: "white",
      borderRadius: 6,
      fs: 15,
      t: [
        {
          fi: { t: "fas fa-user" },
          position: { vertical: 18, horizontal: "left" },
          padding: { top: 0, right: 0, bottom: 0, left: 8 },
          fs: 20,
        },
        {
          t: "Label 1",
          fb: true,
          position: { vertical: "top", horizontal: "right" },
          padding: { top: 8, right: 10, bottom: 0, left: 0 },
          margin: { left: 10 },
        },
        { t: "Label 2", fs: 8, padding: { top: 0, right: 10, bottom: 0, left: 0 } },
        { t: "Label 3", fs: 8, padding: { top: 0, right: 10, bottom: 0, left: 0 } },
      ],
      x: 350,
      y: 10,
    },
    {
      id: "l1",
      type: "link",
      id1: "6",
      id2: "2",
      c: "rgb(145, 146, 149)",
      w: 1,
    },
    {
      id: "l2",
      type: "link",
      id1: "3",
      id2: "8",
      c: "rgb(145, 146, 149)",
      w: 1,
    },
    {
      id: "l3",
      type: "link",
      id1: "6",
      id2: "1",
      t: "Label",
      c: "#c41f30",
      w: 2,
    },
    {
      id: "l4",
      type: "link",
      id1: "4",
      id2: "7",
      c: "rgb(145, 146, 149)",
      w: 1,
      t1: { t: "Label at end" },
      t2: { g: [{ p: "ne", c: "rgb(220, 0, 0)", a: true }] },
    },
    {
      id: "l5",
      type: "link",
      id1: "4",
      id2: "5",
      c: "rgb(145, 146, 149)",
      w: 1,
      t2: { g: [{ p: "ne", c: "rgb(114, 179, 0)", u: "/images/glyphs/check-mark.png" }] },
    },
    {
      id: "l6",
      type: "link",
      id1: "6",
      id2: "3",
      c: "rgb(145, 146, 149)",
      w: 1,
    },
    {
      id: "l7",
      type: "link",
      id1: "3",
      id2: "4",
      c: "rgb(145, 146, 149)",
      w: 1,
    },
  ],
};
<!doctype html>
<html lang="en" style="background-color: #2d383f">
  <head>
    <meta charset="utf-8" />
    <title>Interaction Events</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" href="./style.css" />
  </head>
  <body>
    <div id="klchart" class="klchart"></div>
    <div id="modifier-keys" style="position: absolute; pointer-events: none">
      <span class="key" id="alt">alt</span><span class="key" id="ctrl">ctrl</span
      ><span class="key" id="consistentCtrl">consistentCtrl</span
      ><span class="key" id="meta">meta</span><span class="key" id="shift">shift</span>
    </div>
    <script type="module" src="./code.js"></script>
  </body>
</html>
#events .eventname {
  color: #00aa70;
  font-family:
    Monaco,
    Andale Mono,
    Courier New,
    monospace;
  padding: 4px 5px;
  font-size: 14px;
  cursor: pointer;
}

#filterEvents {
  border-color: #cacdcf;
  border-style: solid;
  border-width: 1px;
  border-radius: 3px;
  padding: 6px;
  cursor: pointer;
  background-color: #fff;
  margin-top: 10px;
}

#filterEvents .icon-chevron-down {
  width: 12px;
  height: 12px;
  border-color: #333;
  border-style: solid;
  border-width: 4px 4px 0px 0px;
  transform: rotate(135deg);
  margin-right: 5px;
}

#eventsListContainer {
  width: 368px;
  border-width: 1px;
  border-color: #cacdcf;
  border-top: 0px;
  border-style: solid;
  border-radius: 3px;
  padding-bottom: 8px;
  background-color: #ffffff;
  position: absolute;
  margin-left: 0px;
  z-index: 1000;
  scrollbar-width: none;
}

#eventsList {
  padding: 6px;
  width: 100%;
}

#eventsList label {
  display: inline-block;
  margin: 4px 40px 4px 0px;
  width: calc(48% - 35px);
  cursor: pointer;
}

#noEvents {
  margin-left: 6px;
}

#eventLog {
  height: 250px;
  overflow-y: auto;
  margin-bottom: 10px;
  clear: both;
  border: 1px solid #ccc;
  background-color: #fff;
  cursor: ns-resize;
}

#eventLog table {
  margin-bottom: 5px;
}

input[type="checkbox"] {
  margin-right: 5px;
}

#events .selectedEvent {
  background-color: #ddd;
}

@media (max-width: 979px) {
  #eventsListContainer {
    width: 326px;
  }

  #eventsList label {
    margin-right: 11px;
    width: calc(50% - 11px);
  }
}

@media (max-width: 767px) {
  #eventsListContainer {
    width: 266px;
  }

  #eventsList label {
    margin-right: 10px;
    width: calc(50% - 10px);
    font-size: 11px;
  }
}

#eventDetails {
  height: 250px;
  width: 100%;
  overflow-y: auto;
}

#modifier-keys {
  opacity: 0;
}

.key {
  background-color: #b2df8a;
  border: 2px solid #ffffff;
  display: inline-block;
  color: #5e5e5e;
  font: 14px arial;
  text-decoration: none;
  text-align: center;
  margin: 3px 3px;
  padding: 6px 6px;
}

.key.active {
  border-color: #ffaf00;
  background-color: #ffaf00;
}

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.