//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//
//     Sample Code
//
//!    Create and edit annotations in your chart.
import KeyLines from "keylines";
import {
  createAnnotation,
  data,
  colours,
  defaultAnnotationsCount,
} from "./annotations-data.js";

let chart;
let annotationIdCount = defaultAnnotationsCount;
let snapshotIdCount = 0;
let nodeIdWithAnnotateButton = null;
let annotationBeingUpdated = null;
let hoveredAnnotationId = null;
let selectedNodes = [];
let annotationToolbarShown = false;
let isNewAnnotation = null;
let handMode = true;
let isMarqueeDragging = false;
let queuedAnnotationContainer = "none";
const editLabelIndex = 2;
const snapshotStore = new Map();
const chartContainer = document.getElementById("klchart");
const annotationTextArea = document.getElementById("annotation-text");
const annotationPreview = document.getElementById("annotation-preview");
const snapshotButton = document.getElementById("snapshot-button");
const annotateButton = document.getElementById("annotate-button");
const textMeasurementSpan = document.getElementById("text-measure");

async function annotate() {
  // Reset the pointer cursor override from the edit label
  setCursor(null);
  await updateNodeColours(true, selectedNodes);
  hideAnnotateButton();
  const annotationSubjects = getSelectedNodeIds();
  isNewAnnotation = true;

  // Add the initial annotation to the chart to help with overlay positioning
  annotationBeingUpdated = createAnnotation(
    `a${++annotationIdCount}`,
    annotationSubjects,
    queuedAnnotationContainer
  );
  await chart.merge(annotationBeingUpdated);

  // Ensure the new annotations and its subjects are in view if required
  await fitViewToAnnotation();
  placeAnnotationPreview();
  setupAnnotationPreview();
}

async function finalizeAnnotation(action) {
  if (annotationBeingUpdated === null) return;

  if (action === undefined || action === "dismiss") {
    const annotationText = annotationBeingUpdated.t[1].t;
    action = annotationText === "" ? "delete" : "dismiss";
  }

  if (action === "delete") {
    chart.removeItem(annotationBeingUpdated.id);
  } else {
    const windowSelection = window.getSelection();
    // Clear text highlighting
    if (windowSelection.rangeCount !== 0) {
      windowSelection.removeAllRanges();
    }
  }

  annotationPreview.style.display = "none";
  await updateNodeColours(false, selectedNodes);
  if (isNewAnnotation && action === "delete") {
    // Maintain the selection when cancelling a new annotation
    placeAnnotateButtonOnNode(getLatestSelectedNodeId());
  } else {
    chart.selection([]);
    /* selection-change does not fire when triggered programmatically
        so we must updated the selectedNodes here */
    selectedNodes = [];
  }

  annotationBeingUpdated = null;
  isNewAnnotation = null;
}

async function updateAnnotationContainer(containerType) {
  if (annotationBeingUpdated === null) return;
  annotationPreview.style.display = "none";
  await chart.setProperties({
    id: annotationBeingUpdated.id,
    connectorStyle: {
      ...annotationBeingUpdated.connectorStyle,
      container: containerType,
      subjectEnd: "dot",
    },
  });

  // Fit the view to the annotation and its subjects
  await fitViewToAnnotation();
  placeAnnotationPreview();
  annotationPreview.style.display = "flex";
}

async function updateAnnotationTextAndHeight(text, height) {
  const annotationBeingUpdatedLabels = annotationBeingUpdated.t;
  annotationBeingUpdatedLabels[1].t = text;
  await chart.setProperties({
    id: annotationBeingUpdated.id,
    t: annotationBeingUpdatedLabels,
    h: height,
  });
}

function getSelectedNodeIds() {
  return selectedNodes.map(({ id }) => id);
}

function getLatestSelectedNodeId() {
  return selectedNodes[selectedNodes.length - 1].id;
}

function setSelectedContainerControl(type) {
  const containerControl = document.getElementById(type);
  containerControl.checked = true;
}

// Sets up the initial state for the preview based on the annotation being edited
function setupAnnotationPreview() {
  const {
    connectorStyle: { container },
    t,
  } = annotationBeingUpdated;
  setSelectedContainerControl(container);
  const annotationText = t[1].t;
  annotationTextArea.value = annotationText;
  toggleAnnotationToolbar(annotationText !== "");
  updateTextAreaHeight();
}

async function onAnnotationKeyDown(e) {
  const { key } = e;
  if (annotationBeingUpdated !== null) {
    if (key === "Escape") {
      finalizeAnnotation();
    }
    if (key === "Enter") {
      // Don't create new lines for enter
      e.preventDefault();
      await finalizeAnnotation("dismiss");
    }
  }
}

function toggleAnnotationToolbar(show) {
  const annotationToolbar = document.getElementById("annotation-toolbar");
  const ghostDoneButton = document.getElementById("ghost-done-button");
  const toolbarDisplay = show ? "flex" : "none";
  const ghostDoneButtonDisplay = show ? "none" : "block";
  annotationToolbar.style.display = toolbarDisplay;
  ghostDoneButton.style.display = ghostDoneButtonDisplay;
  annotationToolbarShown = show;
}

function updateSelectedNodes() {
  const selectedItemIds = chart.selection();
  selectedNodes = selectedItemIds
    .map((itemId) => chart.getItem(itemId))
    .filter((item) => item.type === "node");
}

/* Positions the annotate button based on the current
   chart zoom level and attached node position */
function updateAnnotateButtonPosition() {
  const nodeBaseSize = 26;
  const node = chart.getItem(nodeIdWithAnnotateButton);
  const { x, y } = chart.viewCoordinates(node.x, node.y);
  const { zoom } = chart.viewOptions();
  const nodeSize = nodeBaseSize * (node.e || 1) * zoom;
  annotateButton.style.left = `${x + nodeSize}px`;
  annotateButton.style.top = `${y - annotateButton.clientHeight - nodeSize}px`;
}

function placeAnnotateButtonOnNode(nodeId) {
  nodeIdWithAnnotateButton = nodeId;
  annotateButton.style.display = "block";
  updateAnnotateButtonPosition();
}

function isAnnotationOutOfBounds(id) {
  const annotationPosition = chart.labelPosition(id, 0);
  const chartContainerBounds = chartContainer.getBoundingClientRect();
  const outOfBounds =
    annotationPosition.x1 <= 0 ||
    annotationPosition.x2 >= chartContainerBounds.width;
  return outOfBounds;
}

async function initialiseAnnotationEdit() {
  // Highlight the annotation subjects
  chart.selection(annotationBeingUpdated.subject);
  updateSelectedNodes();
  await updateNodeColours(
    true,
    annotationBeingUpdated.subject.map((subjectId) => chart.getItem(subjectId))
  );
  // await fitViewToAnnotation();
  placeAnnotationPreview();
  // Preconfigure the preview / controls for the annotation we are editing
  setupAnnotationPreview();
}

// Fits the view to the annotation and its subjects if required
async function fitViewToAnnotation() {
  // Check whether we need to adjust the view to allow the annotation space
  if (isAnnotationOutOfBounds(annotationBeingUpdated.id)) {
    await chart.zoom("fit", {
      ids: [
        annotationBeingUpdated.id,
        ...annotationBeingUpdated.subject.map((id) => id),
      ],
      animate: true,
      time: 400,
    });
  }
}

function updateTextAreaHeight() {
  annotationTextArea.style.height = "auto";
  annotationTextArea.style.height = `${annotationTextArea.scrollHeight}px`;
}

/* KeyLines doesn't wrap strings without spaces so add new lines manually
   to handle long words which exceed the annotation body width */
function wrapLongWord(word) {
  let wrappedText = "";
  let tempLine = "";
  for (const char of word) {
    // Add characters to the measurement span incrementally to check if the wo
    textMeasurementSpan.textContent = `${tempLine}${char}`;
    if (
      textMeasurementSpan.getBoundingClientRect().width >
      annotationTextArea.clientWidth
    ) {
      wrappedText += `${tempLine}\n`;
      tempLine = char;
    } else {
      tempLine += char;
    }
  }
  // Add remaining part of word
  wrappedText += tempLine;
  return wrappedText;
}

function formatAnnotationInputText(string) {
  let formattedText = "";
  const words = string.split(" ");
  words.forEach((word) => {
    textMeasurementSpan.textContent = word;
    /* Use the measurement span to check if a word doesn't fit on a single line in the text area 
         if it does wrap it across multiple lines */
    if (
      textMeasurementSpan.getBoundingClientRect().width >
      annotationTextArea.clientWidth
    ) {
      formattedText += wrapLongWord(word) + " ";
    } else {
      formattedText += word + " ";
    }
  });
  return formattedText.trim();
}

async function onAnnotationInput() {
  // Remove any trailing whitespaces and new lines (\n) that may have been added from previous long words
  const cleanTextInput = annotationTextArea.value.replace(/\n/g, "").trim();
  const cleanTextLength = cleanTextInput.length;
  const showToolbar = cleanTextLength > 0 && !annotationToolbarShown;
  // Ensure annotation preview height grows / shrinks with the text input
  updateTextAreaHeight();

  if (showToolbar) {
    toggleAnnotationToolbar(true);
  } else if (annotationToolbarShown && cleanTextLength === 0) {
    // If we only have white space hide the toolbar
    toggleAnnotationToolbar(false);
  }

  // Format text so long words are wrapped correctly before updating chart annotation text
  const formattedText = formatAnnotationInputText(cleanTextInput);
  // Update the chart annotation text & height so we can position the preview
  await updateAnnotationTextAndHeight(
    formattedText,
    annotationPreview.offsetHeight
  );
  // Sync the preview overlay position with the chart annotation position
  placeAnnotationPreview();
}

function placeAnnotationPreview() {
  // Get the position of the annotation's background label to place the preview
  const annotationPosition = chart.labelPosition(annotationBeingUpdated.id, 0);
  annotationPreview.style.top = `${annotationPosition.y1}px`;
  annotationPreview.style.left = `${annotationPosition.x1}px`;
  annotationPreview.style.display = "flex";
  annotationTextArea.focus();
}

// Adds / removes colour styling on nodes being annotated
async function updateNodeColours(active, nodes) {
  const activeNodeUpdates = nodes.map(({ id, fi }) => ({
    id,
    c: active ? colours.active : colours.defaultNodeBase,
    fi: {
      ...fi,
      c: active ? colours.activeFontIcon : colours.defaultFontIcon,
    },
  }));
  // TO-DO: Change this back to animateProperties when bug is fixed
  await chart.setProperties(activeNodeUpdates);
}

function hideAnnotateButton() {
  annotateButton.style.display = "none";
  nodeIdWithAnnotateButton = null;
}

function updateHandMode(navType) {
  handMode = navType === "hand" ? true : false;
  chart.options({ handMode });
}

function createIconSpanEl(className) {
  const iconSpanEl = document.createElement("span");
  iconSpanEl.classList.add(className);
  return iconSpanEl;
}

async function saveSnapshot() {
  const chartState = chart.serialize();
  const chartImage = await chart.export({
    type: "svg",
    fitTo: { width: 1392, height: 856 },
    fonts: {
      "Font Awesome 5 Free": {
        src: "/fonts/fontAwesome5/fa-solid-900.woff",
      },
      "CI-Icons": {
        src: "/fonts/ci-icons/CI-Icons.woff",
      },
      Muli: { src: "/fonts/Muli/Muli-Regular.ttf" },
    },
  });

  const snapshotId = `snapshot-${++snapshotIdCount}`;
  snapshotStore.set(snapshotId, {
    chartState,
    chartImage,
    selectedNodes,
    nodeIdWithAnnotateButton,
  });
  return {
    snapshotId,
    chartImage,
  };
}

function createSnapshotRowControls() {
  const snapshotControls = document.createElement("div");
  snapshotControls.id = "snapshot-controls";
  const downloadButton = document.createElement("button");
  const deleteButton = document.createElement("button");
  const downloadIcon = createIconSpanEl("ci-icon-download");
  const deleteIcon = createIconSpanEl("ci-icon-bin");
  const downloadLink = document.createElement("a");

  downloadLink.appendChild(downloadIcon);
  downloadButton.appendChild(downloadLink);
  deleteButton.appendChild(deleteIcon);
  snapshotControls.appendChild(downloadButton);
  snapshotControls.appendChild(deleteButton);

  downloadButton.addEventListener("click", (e) => onDownloadSnapshot(e));
  deleteButton.addEventListener("click", (e) => onDeleteSnapshot(e));
  return snapshotControls;
}

function createSnapshotRowInfo() {
  const snapshotInfo = document.createElement("div");
  snapshotInfo.classList.add("snapshot-info");
  const snapshotIdEl = document.createElement("span");
  snapshotIdEl.textContent = `Snapshot ${snapshotIdCount}`;
  snapshotIdEl.style.fontWeight = "bold";
  snapshotInfo.appendChild(snapshotIdEl);
  return snapshotInfo;
}

function createSnapshotRow(snapshotId, chartImage) {
  const snapshotRow = document.createElement("div");
  snapshotRow.classList.add("snapshot-row");
  snapshotRow.id = snapshotId;
  const snapshotImgContainer = document.createElement("div");
  snapshotImgContainer.classList.add("snapshot-img-container");
  const snapshotImageEl = document.createElement("img");
  snapshotImageEl.src = chartImage.url;
  snapshotImgContainer.appendChild(snapshotImageEl);
  const snapshotInfo = createSnapshotRowInfo(snapshotId);
  const snapshotControls = createSnapshotRowControls(snapshotId, snapshotRow);
  snapshotInfo.appendChild(snapshotControls);
  snapshotRow.appendChild(snapshotImgContainer);
  snapshotRow.appendChild(snapshotInfo);
  snapshotRow.addEventListener("click", onLoadSnapshot);
  // Preprend a row with controls to the snapshot list
  const snapshotList = document.getElementById("snapshot-list");
  snapshotList.prepend(snapshotRow);
}

async function onCaptureSnapshot() {
  // Ensure in progress annotations are finalized before snapshotting
  if (annotationBeingUpdated) {
    await finalizeAnnotation();
  }
  snapshotButton.disabled = true;
  const { snapshotId, chartImage } = await saveSnapshot();
  createSnapshotRow(snapshotId, chartImage);

  // Show a preview thumbnail image of the snapshot
  const snapshotPreviewContainer = document.getElementById("snapshot-preview");
  snapshotPreviewContainer.style.display = "block";
  snapshotPreviewContainer.classList.add("fade-in-out");
  snapshotPreviewContainer.querySelector("img").src = chartImage.url;

  setTimeout(() => {
    snapshotPreviewContainer.classList.remove("fade-in-out");
    snapshotPreviewContainer.style.display = "none";
    snapshotButton.disabled = false;
  }, 2000);
}

function getClickedSnapshotId(e) {
  const snapshotRow = e.target.closest("div.snapshot-row");
  return snapshotRow.id;
}

async function onLoadSnapshot(e) {
  if (annotationBeingUpdated !== null) {
    finalizeAnnotation();
  }
  const snapshotId = getClickedSnapshotId(e);

  // Load the serialized chart from the snapshotStore
  const snapshot = snapshotStore.get(snapshotId);
  await chart.load(snapshot.chartState);

  // Update the selection and the annotate button
  selectedNodes = snapshot.selectedNodes;
  chart.selection(getSelectedNodeIds());
  if (selectedNodes.length > 0) {
    placeAnnotateButtonOnNode(getLatestSelectedNodeId());
  } else {
    hideAnnotateButton();
  }
}

function onDeleteSnapshot(e) {
  e.stopPropagation();
  const snapshotId = getClickedSnapshotId(e);
  const snapshotRow = document.getElementById(snapshotId);
  const [downloadButton, deleteButton] = snapshotRow.querySelectorAll("button");

  // Clean up snapshot row event listeners and remove the row
  snapshotRow.removeEventListener("click", onLoadSnapshot);
  downloadButton.removeEventListener("click", onDownloadSnapshot);
  deleteButton.removeEventListener("click", onDeleteSnapshot);
  snapshotRow.remove();

  snapshotStore.delete(snapshotId);
}

function onDownloadSnapshot(e) {
  e.stopPropagation();
  const snapshotId = getClickedSnapshotId(e);
  const {
    chartImage: { url },
  } = snapshotStore.get(snapshotId);
  const snapshotLink = document.createElement("a"); // create the link to download the image
  snapshotLink.download = `${snapshotId}.svg`;
  snapshotLink.href = url;
  snapshotLink.click();
  URL.revokeObjectURL(snapshotLink.download);
}

// Enable cursor styling override while interacting with the chart
function setCursor(type) {
  // get all non-cursor classes
  const classes = Array.from(chartContainer.classList).filter(
    (cn) => !cn.match(/^cursor-/)
  );
  if (type) {
    classes.push(`cursor-${type}`);
  }
  chartContainer.className = classes.join(" ");
}

async function onSelectionChange() {
  updateSelectedNodes();

  // remove the glyph from the previous selected node if we have one
  if (nodeIdWithAnnotateButton) {
    hideAnnotateButton();
  }
  if (selectedNodes.length > 0) {
    placeAnnotateButtonOnNode(getLatestSelectedNodeId());
  }

  // Base the container shape of the next annotation on whether marquee selection was last used
  queuedAnnotationContainer = isMarqueeDragging ? "rectangle" : "none";
}

async function onClick({ id, subItem, preventDefault }) {
  if (annotationBeingUpdated !== null) {
    finalizeAnnotation();
    preventDefault();
    return;
  }

  if (id) {
    const clickedItem = chart.getItem(id);
    if (
      clickedItem.type === "annotation" &&
      subItem?.index === editLabelIndex
    ) {
      if (nodeIdWithAnnotateButton !== null) {
        hideAnnotateButton();
      }
      // Prevent selection from being cleared when an annotation is clicked
      preventDefault();
      setCursor(null);
      annotationBeingUpdated = clickedItem;
      await initialiseAnnotationEdit();
    }
  }
}

async function onDragStart({ type }) {
  if (type === "marquee") isMarqueeDragging = true;
  if (annotationBeingUpdated !== null) {
    await finalizeAnnotation();
  }
}

function onDragEnd({ type }) {
  if (type === "marquee") isMarqueeDragging = false;
}

function onDragMove({ id }) {
  const selectedNodeIds = getSelectedNodeIds();
  // Update the annotate button position while dragging the attached node
  if (id && selectedNodeIds.includes(id)) {
    updateAnnotateButtonPosition();
  }
}

function onWheel({ preventDefault }) {
  // Prevent zooming the chart while updating an annotation
  if (annotationBeingUpdated !== null) {
    preventDefault();
  }
}

function onViewChange() {
  // Update the annotate button position while zooming and panning the chart
  if (nodeIdWithAnnotateButton) {
    updateAnnotateButtonPosition();
  }
}

/* Ensures annotate button position is updated when nodes
   are moved using the keyboard arrow keys */
function onPreChange({ type }) {
  if (type === "move" && nodeIdWithAnnotateButton !== null) {
    setTimeout(() => {
      updateAnnotateButtonPosition();
    });
  }
}

async function onHover({ id, subItem }) {
  const hoveringEditLabel = id && subItem?.index === editLabelIndex;
  if (hoveringEditLabel) {
    hoveredAnnotationId = id;
  }
  if (hoveredAnnotationId !== null) {
    await updateEditLabelHoverState(
      hoveringEditLabel,
      chart.getItem(hoveredAnnotationId)
    );
    const cursorUpdateType = hoveringEditLabel ? "pointer" : "null";
    setCursor(cursorUpdateType);
  }
  if (id === null) {
    hoveredAnnotationId = null;
  }
}

async function updateEditLabelHoverState(hovered, hoveredAnnotation) {
  const hoveredAnnotationLabel = hoveredAnnotation?.t;
  if (hoveredAnnotationLabel) {
    const editLabelHoverColour = hovered
      ? colours.hoveredEditLabel
      : colours.defaultEditLabel;
    hoveredAnnotation.t[editLabelIndex].fbc = editLabelHoverColour;
    await chart.setProperties({
      id: hoveredAnnotation.id,
      t: hoveredAnnotation.t,
    });
  }
}

function initialiseInteractions() {
  chart.on("selection-change", onSelectionChange);
  chart.on("click", onClick);
  chart.on("hover", onHover);
  chart.on("drag-start", onDragStart);
  chart.on("drag-end", onDragEnd);
  chart.on("drag-move", onDragMove);
  chart.on("wheel", onWheel);
  chart.on("view-change", onViewChange);
  chart.on("prechange", onPreChange);
  const navControls = document.querySelectorAll("input[name=nav-controls]");
  const containerControls = document.querySelectorAll(
    "input[name=container-controls]"
  );
  const confirmAnnotationUpdateButton = document.getElementById("done");
  const deleteAnnotationButton = document.getElementById("delete");
  const chartCanvas = document.querySelector("canvas");

  navControls.forEach((navControl) => {
    navControl.addEventListener("change", (e) => {
      updateHandMode(e.target.value);
    });
  });

  annotateButton.addEventListener("click", annotate);

  containerControls.forEach((containerControl) => {
    containerControl.addEventListener("change", async (e) => {
      await updateAnnotationContainer(e.target.value);
    });
  });

  annotationTextArea.addEventListener("input", onAnnotationInput);
  annotationTextArea.addEventListener("keydown", onAnnotationKeyDown);
  snapshotButton.addEventListener("click", onCaptureSnapshot);
  confirmAnnotationUpdateButton.addEventListener("click", () =>
    finalizeAnnotation("dismiss")
  );
  deleteAnnotationButton.addEventListener("click", () =>
    finalizeAnnotation("delete")
  );

  /* Pass the wheel event to the keylines chart to enable zooming 
      while hovering the annotate button */
  annotateButton.addEventListener(
    "wheel",
    (e) => {
      e.preventDefault();
      chartCanvas.dispatchEvent(new WheelEvent("wheel", e));
    },
    { passive: false }
  );

  // End annotation process on window resize
  window.addEventListener("resize", () => {
    if (annotationBeingUpdated !== null) {
      finalizeAnnotation();
    }
  });
}

async function startKeyLines() {
  const options = {
    handMode: true,
    navigation: false,
    overview: false,
    minZoom: 0.28,
    hover: 20,
    fontFamily: "Muli",
    iconFontFamily: "Font Awesome 5 Free",
    imageAlignment: {
      "fas fa-user": { e: 0.7 },
      "fas fa-building": { e: 0.7 },
    },
    selectionColour: colours.active,
    selectionFontColour: "rgb(0, 0, 0)",
    gradient: {
      stops: [
        { r: 0, c: "rgb(237, 234, 254)" },
        { r: 0.47, c: "rgb(233, 244, 255)" },
        { r: 1, c: "rgb(224, 248, 255)" },
      ],
    },
    selectedNode: {
      ha0: {
        c: colours.active,
        r: 30,
        w: 1,
      },
    },
    selectedLink: {},
    linkEnds: {
      avoidLabels: true,
      spacing: "loose",
    },
  };

  chart = await KeyLines.create({ container: "klchart", options });
  await chart.load(data);
  await chart.layout("lens", { consistent: true });
  const { snapshotId, chartImage } = await saveSnapshot();
  createSnapshotRow(snapshotId, chartImage);

  initialiseInteractions();
}

function loadWebFonts() {
  Promise.all([
    document.fonts.load("24px 'Font Awesome 5 Free'"),
    document.fonts.load("24px 'CI-Icons'"),
  ]).then(startKeyLines);
}

window.addEventListener("DOMContentLoaded", loadWebFonts);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
  <head>
    <meta charset="UTF-8">
    <title>Annotating 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="/css/ci-icons.css">
    <link rel="stylesheet" type="text/css" href="/annotations.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="/annotations.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 class="overlay" oncontextmenu="return false;">
              <div class="static-overlay">
                <div class="overlay-toolbar">
                  <div class="switch" id="nav-controls">
                    <input type="radio" name="nav-controls" id="marquee" value="marquee">
                    <label class="ci-icon-marquee" for="marquee"></label>
                    <input type="radio" name="nav-controls" id="cursor" value="hand" checked>
                    <label class="ci-icon-cursor" for="cursor"></label>
                  </div>
                  <button class="overlay-button static-overlay-button" id="snapshot-button"><span class="ci-icon-snapshot"></span></button>
                </div>
                <div id="snapshot-preview"><img draggable="false" alt="A chart preview export"></div>
              </div>
              <button class="overlay-button" id="annotate-button"><span class="ci-icon-annotate"></span></button>
              <div id="annotation-preview" oncontextmenu="return false;">
                <div id="annotation-upper">
                  <textarea id="annotation-text" placeholder="Add comment" rows="1" spellcheck="false"></textarea>
                  <button class="control-button" id="ghost-done-button" disabled><span class="ci-icon-check"></span></button>
                </div>
                <div id="annotation-toolbar">
                  <div class="switch" id="container-controls">
                    <input type="radio" name="container-controls" id="none" value="none" checked>
                    <label class="ci-icon-diagonal-line" for="none"></label>
                    <input type="radio" name="container-controls" id="circle" value="circle">
                    <label class="ci-icon-circle" for="circle"></label>
                    <input type="radio" name="container-controls" id="rectangle" value="rectangle">
                    <label class="ci-icon-rectangle" for="rectangle"></label>
                  </div>
                  <div id="edit-controls">
                    <button class="overlay-button" id="delete"><span class="ci-icon-bin"></span></button>
                    <button class="overlay-button" id="done"> <span class="ci-icon-check"></span></button>
                  </div>
                </div>
              </div><span id="text-measure"></span>
            </div>
          </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%;">Annotating Charts</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">
              <div class="cicontent">
                <p>Select or multi-select nodes and click the<span class="overlay-button inline-annotate-button" style="padding: 0px 0px 0px 6px;"><i class="ci-icon-annotate"></i></span>button to annotate them.</p>
                <p>Each annotation has a <span class="overlay-button inline-edit-button" style="padding: 0px 0px 0px 6px;"><i class="ci-icon-pencil-edit"></i></span>button to edit the text and to toggle between different shapes of subject containers.</p>
                <p>Once you've made your changes, click the<span class="overlay-button snapshot-button inline-snapshot-button" style="padding: 1px 0px 0px 5px;"><i class="ci-icon-snapshot"></i></span>button in the top right chart corner to save the chart state.</p>
                <h4 style="padding: 10px 0px 0px 0px">Snapshots</h4>
                <p>Click the snapshot to restore it in the chart.</p>
                <div id="snapshot-list"></div>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
    <div id="moreParent">
      <div id="moreContainer">
      </div>
    </div>
    <div id="lazyScripts">
    </div>
  </body>
</html>
.overlay {
  position: absolute;
  width: 100%;
  height: 100%;
  overflow: hidden;
  /* Allow chart interaction through the overlay div*/
  pointer-events: none;
}

/* Includes the toolbar and snapshot preview image */
.static-overlay {
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.overlay-toolbar {
  display: flex;
  width: 100%;
  box-sizing: border-box;
  justify-content: space-between;
}

/* Enable pointer events on the toolbar controls themselves */
.overlay-toolbar * {
  pointer-events: auto;
  z-index: 500;
}

/* Enable cursor overriding while hovering the chart */
.cursor-pointer canvas {
  cursor: pointer !important;
}

/* Radio Controls used for nav controls
   and annotation container setting  */
.switch {
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Hide the radio input and only
   show the label as an icon  */
.switch input {
  display: none !important;
}

.switch label {
  display: flex !important;
  font-size: 17px;
  user-select: none;
  align-items: center !important;
  justify-content: center !important;
  background-image: none;
}

.switch label:first-of-type {
  border-radius: 6px 0px 0px 6px;
}

.switch label:last-of-type {
  border-radius: 0px 6px 6px 0px;
}

#nav-controls label {
  width: 38px;
  height: 38px;
  background-color: rgb(248, 248, 248);
  color: black;
}

#nav-controls :checked + label {
  background-color: rgb(0, 163, 255);
}

#nav-controls :not(:checked) + label:hover {
  background-color: rgba(84, 193, 255, 1);
}

.switch :not(:checked) + label {
  cursor: pointer !important;
}

#nav-controls > .ci-icon-cursor {
  font-size: 12px;
}

#nav-controls > .ci-icon-marquee {
  font-size: 22px;
}

.overlay-button {
  background-color: rgb(0, 163, 255);
  border: none;
  cursor: pointer !important;
  color: black;
  &:hover {
    background-color: rgb(84, 193, 255);
  }

  &:not(:disabled):hover {
    background-color: rgb(84, 193, 255) !important;
    > span {
      color: rgb(0, 0, 0);
    }
  }

  &:disabled {
    background-color: rgba(0, 163, 255);
    border: none;
    cursor: not-allowed !important;
    color: black !important;

    &:hover {
      border: none;
      background-color: rgba(0, 163, 255);
    }
  }

  &:focus {
    background-color: rgba(0, 163, 255) !important;
    color: black !important;
  }
}

.static-overlay-button {
  font-size: 16px;
  padding: 8px 16px 8px 16px;
  border-radius: 6px !important;
}

#snapshot-button {
  width: 38px;
  height: 38px;
  padding: 2px 1px 0px 0px;
  font-size: 14px;
}

/* Overlay button (position (top/left) is updated from JS) */
#annotate-button {
  position: absolute;
  z-index: 1000;
  width: 32px;
  height: 32px;
  border-radius: 20px 20px 20px 0px !important;
  padding: 0px;
  pointer-events: auto;
  display: none;
}

/* Preview overlay*/
#annotation-preview {
  display: none; /* Initially none otherwise flex */
  position: absolute;
  flex-direction: column;
  padding: 10px;
  border-radius: 3px;
  width: 274px;
  max-width: 274px;
  gap: 4px;
  background-color: rgb(255, 255, 255);
  z-index: 499;
  box-sizing: border-box;
  pointer-events: auto;
}

#annotation-upper {
  display: flex;
  align-items: center;
}

#annotation-toolbar {
  display: none;
  justify-content: space-between;
  gap: 50px;
  padding: 0px;
  width: 100%;
}

#edit-controls {
  display: flex;
  gap: 6px;
}

#edit-controls > button {
  background-color: rgb(248, 248, 248);
  cursor: pointer;
  border: none;
  width: 26px;
  height: 26px;
  padding: 0px;
  border-radius: 6px !important;

  &:hover {
    background-color: (84, 193, 255);
  }
}

#annotation-text {
  resize: none;
  border: none;
  font-family: 'Muli';
  width: 100%;
  height: auto;
  overflow: hidden;
  line-height: 16px;
  font-size: 14px;
  background-color: rgb(255, 255, 255);
  &:focus {
    outline: none;
  }
}

#text-measure {
  font-family: 'Muli';
  line-height: 15px;
  font-size: 14px;
  padding: 2px;
  visibility: hidden;
}

/* Place holder disabled button for when we first
   create an empty annotation */
#ghost-done-button {
  width: 26px;
  height: 26px;
  padding: 0px;
  background-color: #213d450d;
  border: none;
  border-radius: 6px;
  cursor: auto;

  &:disabled > span {
    color: black !important;
  }
}

.ci-icon-check {
  font-size: 10px;
  margin-top: 4px;
}

.ci-icon-bin {
  font-size: 13px;
  margin-top: 2px;
}

#container-controls label {
  width: 26px;
  height: 26px;
  font-size: 14px;
}

#container-controls :checked + label {
  background-color: rgb(206, 234, 251);
}

#container-controls :not(:checked):hover + label {
  background-color: rgb(84, 193, 255);
}

#snapshot-preview {
  display: none;
  background-color: rgb(255, 255, 255);
  border: 1px solid rgb(0, 0, 0) !important;
  border-radius: 3px;
  width: 348px;
  height: 214px;
  align-self: flex-end;
  pointer-events: auto;
  z-index: 1001;
  box-sizing: content-box;
}

.fade-in-out {
  opacity: 1;
  animation: fade 2s ease-out;
}

@keyframes fade {
  0%,
  100% {
    opacity: 0;
  }
  20%,
  75% {
    opacity: 1;
  }
}

.snapshot-row {
  display: flex;
  justify-content: flex-start;
  gap: 20px;
  height: 96px;
  text-align: left;
  margin: 0px;

  &:hover {
    cursor: pointer;
    background-color: rgba(0, 153, 104, 0.1);

    .snapshot-img-container {
      padding: 4px 9px 4px 4px;
    }
  }
}

#snapshot-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.snapshot-img-container img {
  height: 100%;
  object-fit: contain;
  background-color: rgb(255, 255, 255);
}

.snapshot-info {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 8px 0px 8px 0px;
}

#snapshot-controls {
  display: flex;
  gap: 20px;

  > button {
    padding: 4px !important;
    width: 26px;
    height: 26px;
    border: none;
    background-color: transparent !important;

    span {
      color: rgb(0, 153, 104);
    }

    &:hover {
      background-color: rgba(255, 255, 255, 0.4) !important;
    }
  }
}

.cicontent > p > span {
  display: inline-block;
  width: 22px;
  height: 22px;
  vertical-align: bottom;
  pointer-events: none;
  background-color: rgb(234, 234, 234);

  &:hover {
    background-color: initial;
  }

  > i {
    font-size: 10px;
  }
}

.inline-snapshot-button {
  border-radius: 2px;
  margin: 0px 5px 0px 4px;
}

.inline-edit-button {
  margin: 0px 6px 0px 4px;
}

.inline-annotate-button {
  border-radius: 20px 20px 20px 0px;
  margin: 0px 4px 0px 5px;
}

/* Playground specific styling */
#demorhscontent {
  top: 74px !important;
}

.snapshot-preview-playground {
  align-self: flex-start !important;
}
Loading source