//
// 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
