Drag on the timeline to select events to annotate, or click individual events then click the "Annotate Selected" button. Use click + shift or drag + shift to add to the current selection.
Create the annotation by passing the event IDs from timeline.eventSelection() to the subject parameter of the annotation object.
See also:
Drag on the timeline to select events to annotate, or click individual events then click the "Annotate Selected" button. Use click + shift or drag + shift to add to the current selection.
Create the annotation by passing the event IDs from the eventSelection prop to the subject parameter of the annotation object.
See also:
import { createTimeline } from "kronograph";
import data from "./data";
function byId(id) {
return document.getElementById(id);
}
const timelineElement = byId("my-timeline");
const annotationEditor = byId("annotation-editor");
const annotationEditorText = byId("annotation-editor-text");
const timeline = createTimeline("my-timeline");
let editedAnnotationId = null;
let editedAnnotationSubjects;
let annotationsCreated = 0;
const timelineOptions = {
backgroundColor: "#2a333c",
entities: {
showLines: "individualEvents",
},
controls: {
editAnnotation: {
show: true,
},
},
};
function hideAnnotationEditor() {
if (annotationEditor) {
annotationEditor.style.display = "none";
annotationEditor.style.width = "175px";
annotationEditorText.style.height = "58px";
}
editedAnnotationId = null;
}
function annotationEditorSave() {
if (annotationEditorText) {
const label = annotationEditorText.value;
if (label.length > 0) {
const newAnnotation = {
connectorStyle: { container: "roundrect", mergeContainers: timeline.isHeatmapShown() },
label,
subject: editedAnnotationSubjects,
position: { angle: 45, distance: 150 },
};
const annotationId = editedAnnotationId ?? `a${annotationsCreated++}`;
timeline.annotations({ ...timeline.annotations(), [annotationId]: newAnnotation });
} else if (editedAnnotationId !== null) {
const annotations = timeline.annotations();
delete annotations[editedAnnotationId];
timeline.annotations(annotations);
}
hideAnnotationEditor();
}
}
function editAnnotation() {
if (annotationEditorText && annotationEditor && editedAnnotationId) {
const annotation = timeline.annotations()[editedAnnotationId];
editedAnnotationSubjects = annotation.subject;
const label = annotation.label;
annotationEditorText.value = label;
const annotationPosition = timeline.getAnnotationBodyPosition(editedAnnotationId);
if (annotationPosition) {
const { x1, y1, x2, y2 } = annotationPosition;
const left = x1 - 5;
const top = y1 - 5;
const annotationWidth = x2 - x1;
const annotationHeight = y2 - y1;
annotationEditor.style.display = "block";
annotationEditor.style.left = `${left}px`;
annotationEditor.style.top = `${top}px`;
annotationEditor.style.width = `${Math.max(annotationWidth * 1.15, 175)}px`;
annotationEditorText.style.height = `${annotationHeight}px`;
annotationEditorText.setSelectionRange(0, label.length);
}
}
}
function getEditorPosition(x, y) {
const bounds = timelineElement.getBoundingClientRect();
let left = x ? x + 10 : bounds.left + ((bounds.right - bounds.left) / 2 - 110);
let top = y ? y - 100 : bounds.top + ((bounds.bottom - bounds.top) / 2 - 110);
if (left + 180 > bounds.width) {
left = bounds.width - 180;
}
if (left < 0) {
left = 10;
}
if (top + 100 > bounds.height) {
top = bounds.height - 110;
}
if (top < 0) {
top = 10;
}
return {
top,
left,
};
}
function createAnnotation(subjects, x, y) {
if (annotationEditor && annotationEditorText) {
editedAnnotationSubjects = subjects;
const label = "New annotation";
annotationEditorText.value = label;
const editorPosition = getEditorPosition(x, y);
annotationEditor.style.display = "block";
annotationEditor.style.left = `${editorPosition.left}px`;
annotationEditor.style.top = `${editorPosition.top}px`;
annotationEditorText.focus();
annotationEditorText.setSelectionRange(0, label.length);
}
}
function createAnnotationFromSelection() {
const eventSelections = timeline.eventSelection();
const firstEventPosition = timeline.getEventPosition(eventSelections[0]);
createAnnotation(eventSelections, firstEventPosition.x1, firstEventPosition.y1);
}
timeline.on("event-selection-change", ({ eventSelection }) => {
if (eventSelection.length > 0) {
byId("annotate-selected").removeAttribute("disabled");
} else {
byId("annotate-selected").setAttribute("disabled", true);
}
});
timeline.on("drag-end", ({ dragType, entityRangeSubject, x, y }) => {
if (dragType === "marqueeSelection") {
if (timeline.eventSelection().length > 0) {
const eventIds = timeline.eventSelection();
if (eventIds.length > 0) {
createAnnotation(eventIds, x, y);
}
} else if (entityRangeSubject !== undefined && entityRangeSubject.id.length > 0) {
createAnnotation(entityRangeSubject, x, y);
}
}
});
timeline.on("click", ({ id, targetType }) => {
if (targetType === "annotationEditButton") {
editedAnnotationId = id;
editAnnotation();
}
});
timeline.on("range", hideAnnotationEditor);
timeline.options(timelineOptions);
timeline.set(data);
timeline.fit();
timeline.eventSelectionMode(true);
byId("annotation-editor-save")?.addEventListener("click", annotationEditorSave);
byId("annotation-editor-cancel")?.addEventListener("click", hideAnnotationEditor);
byId("annotation-editor")?.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
byId("annotation-editor-save")?.click();
}
});
byId("annotate-selected").addEventListener("click", createAnnotationFromSelection); export default {
entities: {
"smith-johnathan-151": {
label: "John Smith",
},
"west-josephine-126": {
label: "Josephine West",
},
"roberts-nathaniel-023": {
label: "Nathan Roberts",
},
"baxter-eleanor-004": {
label: "Ella Baxter",
},
},
events: {
"Email 1": {
entityIds: ["smith-johnathan-151", "roberts-nathaniel-023"],
time: new Date(2025, 7, 14, 8, 49),
},
"Email 2": {
entityIds: ["smith-johnathan-151", "baxter-eleanor-004"],
time: new Date(2025, 7, 14, 8, 56),
},
"Email 3": {
entityIds: ["smith-johnathan-151", "west-josephine-126"],
time: new Date(2025, 7, 14, 9, 2),
},
"Email 4": {
entityIds: ["smith-johnathan-151", "west-josephine-126"],
time: new Date(2025, 7, 14, 9, 27),
},
"Email 5": {
entityIds: ["baxter-eleanor-004", "roberts-nathaniel-023"],
time: new Date(2025, 7, 14, 10, 20),
},
"Email 6": {
entityIds: ["roberts-nathaniel-023", "smith-johnathan-151"],
time: new Date(2025, 7, 14, 10, 30),
},
"Email 7": {
entityIds: ["west-josephine-126", "smith-johnathan-151"],
time: new Date(2025, 7, 14, 10, 40),
},
"Email 8": {
entityIds: ["smith-johnathan-151", "west-josephine-126"],
time: new Date(2025, 7, 14, 10, 51),
},
"Email 9": {
entityIds: ["west-josephine-126", "smith-johnathan-151"],
time: new Date(2025, 7, 14, 12, 5),
},
"Email 10": {
entityIds: ["smith-johnathan-151", "west-josephine-126"],
time: new Date(2025, 7, 14, 14, 37),
},
"Email 11": {
entityIds: ["baxter-eleanor-004", "west-josephine-126"],
time: new Date(2025, 7, 14, 16, 7),
},
"Email 12": {
entityIds: ["west-josephine-126", "roberts-nathaniel-023"],
time: new Date(2025, 7, 14, 17, 42),
},
},
}; <!doctype html>
<html>
<head>
<link rel="stylesheet" href="@ci/theme/kg/css/examples.css" />
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div class="story">
<div class="story__timeline" id="my-timeline"></div>
<div id="annotation-controls" class="story__controls story__controls--row">
<div class="story__controls__item">
<button id="annotate-selected" class="button" type="button" disabled>
Annotate Selected
</button>
</div>
</div>
<div id="annotation-editor">
<textarea id="annotation-editor-text"></textarea>
<div class="annotation-editor-button-container">
<button id="annotation-editor-save" class="annotation-editor-button">
<i class="fas fa-check"></i>
</button>
<button id="annotation-editor-cancel" class="annotation-editor-button">
<i class="fas fa-times"></i>
</button>
</div>
</div>
</div>
<script type="module" src="./code.js"></script>
</body>
</html> .timeline-wrapper,
.story__timeline {
height: 100%;
}
#annotation-editor {
background-color: #242c35;
display: none;
max-height: 300px;
max-width: 300px;
overflow-y: auto;
position: absolute;
border-radius: 8px;
}
#annotation-editor-text {
background-color: #242c35;
width: 100%;
color: #e5e8eb;
font-family: sans-serif;
font-size: 14px;
padding: 10px 10px 0 10px;
/* remove border on focus */
border: none;
overflow: auto;
outline: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
min-height: 58px;
resize: none; /*remove the resize handle on the bottom right*/
}
.annotation-editor-button {
background-color: #3e414b;
color: #afafaf;
border: none;
padding-block: 0;
padding-inline: 0;
height: 26px;
width: 26px;
margin: 5px;
}
.annotation-editor-button:hover {
background-color: #565a69;
}
.annotation-editor-button-container {
display: flex;
justify-content: right;
}
.switch {
position: relative;
display: inline-block;
width: 25px;
height: 16px;
padding: 2px;
vertical-align: middle;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ffffff;
-webkit-transition: 0.4s;
transition: 0.4s;
}
.slider:before {
position: absolute;
content: "";
height: 12px;
width: 12px;
left: 2px;
bottom: 2px;
background-color: #1f1f1f;
-webkit-transition: 0.4s;
transition: 0.4s;
}
input:checked + .slider {
background-color: #00a3ff;
}
input:focus + .slider {
box-shadow: 0 0 1px #00a3ff;
}
input:checked + .slider:before {
-webkit-transform: translateX(9px);
-ms-transform: translateX(9px);
transform: translateX(9px);
}
/* Rounded sliders */
.slider.round {
border-radius: 99px;
}
.slider.round:before {
border-radius: 50%;
}
.slider-label {
padding-left: 0.5rem;
vertical-align: middle;
} import React, { useCallback, useRef, useState } from "react";
import { createRoot } from "react-dom/client";
import data from "./data";
import Timeline from "kronograph/react/Timeline";
const timelineOptions = {
backgroundColor: "#2a333c",
entities: {
showLines: "individualEvents",
},
controls: {
editAnnotation: {
show: true,
},
},
};
export const Demo = () => {
const timeline = useRef(null);
const timelineContainer = useRef(null);
const annotationEditor = useRef(null);
const annotationEditorText = useRef(null);
const annotationEditorSaveElement = useRef(null);
const editedAnnotationId = useRef(null);
const editedAnnotationSubjects = useRef([]);
const annotationsCreated = useRef(0);
const [eventSelections, setEventSelections] = useState([]);
const [annotations, setAnnotations] = useState({});
function hideAnnotationEditor() {
if (annotationEditor.current) {
annotationEditor.current.style.display = "none";
annotationEditor.current.style.width = "175px";
annotationEditorText.current.style.height = "58px";
}
editedAnnotationId.current = null;
}
const annotationEditorSave = useCallback(() => {
if (annotationEditorText.current) {
const label = annotationEditorText.current?.value ?? "";
if (label.length > 0) {
if (editedAnnotationId.current === null) {
const newAnnotation = {
connectorStyle: {
container: "roundrect",
mergeContainers: timeline.current?.isHeatmapShown(),
},
label,
subject: editedAnnotationSubjects.current,
position: { angle: 45, distance: 150 },
};
const newId = `a${annotationsCreated.current}`;
annotationsCreated.current++;
setAnnotations({ ...annotations, [newId]: newAnnotation });
} else {
const existingAnnotation = annotations[editedAnnotationId.current];
if (existingAnnotation) {
setAnnotations({
...annotations,
[editedAnnotationId.current]: { ...existingAnnotation, label },
});
}
}
} else if (editedAnnotationId.current !== null) {
const newAnnotations = { ...annotations };
delete newAnnotations[editedAnnotationId.current];
setAnnotations(newAnnotations);
}
hideAnnotationEditor();
}
}, [annotations]);
function editAnnotation() {
if (annotationEditorText.current && annotationEditor.current && editedAnnotationId.current) {
const annotation = annotations[editedAnnotationId.current];
editedAnnotationSubjects.current = annotation.subject;
const label = annotation.label;
annotationEditorText.current.value = label;
const annotationPosition = timeline.current?.getAnnotationBodyPosition(
editedAnnotationId.current ?? ""
);
if (annotationPosition) {
const { x1, y1, x2, y2 } = annotationPosition;
const left = x1 - 5;
const top = y1 - 5;
const annotationWidth = x2 - x1;
const annotationHeight = y2 - y1;
annotationEditor.current.style.display = "block";
annotationEditor.current.style.left = `${left}px`;
annotationEditor.current.style.top = `${top}px`;
annotationEditor.style.width = `${Math.max(annotationWidth * 1.15, 175)}px`;
annotationEditorText.current.style.height = `${annotationHeight}px`;
annotationEditorText.current.focus();
annotationEditorText.current.setSelectionRange(0, label.length);
}
}
}
function getEditorPosition(x, y) {
const bounds = timelineContainer.current.getBoundingClientRect();
let left = x ? x + 10 : bounds.left + ((bounds.right - bounds.left) / 2 - 120);
let top = y ? y - 100 : bounds.top + ((bounds.bottom - bounds.top) / 2 - 120);
if (left + 180 > bounds.width) {
left = bounds.width - 180;
}
if (left < 0) {
left = 10;
}
if (top + 100 > bounds.height) {
top = bounds.height - 110;
}
if (top < 0) {
top = 10;
}
return {
top,
left,
};
}
function createAnnotation(subjects, x, y) {
if (annotationEditorText.current && annotationEditor.current) {
editedAnnotationSubjects.current = subjects;
const label = "New annotation";
annotationEditorText.current.value = label;
const editorPosition = getEditorPosition(x, y);
annotationEditor.current.style.display = "block";
// offset tooltip 10px from cursor
annotationEditor.current.style.left = `${editorPosition.left}px`;
annotationEditor.current.style.top = `${editorPosition.top}px`;
annotationEditorText.current.focus();
annotationEditorText.current.setSelectionRange(0, label.length);
}
}
function onDragEndHandler({ dragType, entityRangeSubject, x, y }) {
if (dragType === "marqueeSelection") {
if (eventSelections.length > 0) {
createAnnotation(eventSelections, x, y);
} else if (entityRangeSubject !== undefined && entityRangeSubject.id.length > 0) {
createAnnotation(entityRangeSubject, x, y);
}
}
}
function onClickEventHandler({ targetType, id }) {
if (targetType === "annotationEditButton") {
editedAnnotationId.current = id;
editAnnotation();
}
}
function annotateSelection() {
const firstEventPosition = timeline.current.getEventPosition(eventSelections[0]);
createAnnotation(eventSelections, firstEventPosition.x1, firstEventPosition.y1);
}
return (
<div className="story">
<div className="timeline-wrapper" ref={timelineContainer}>
<Timeline
entities={data.entities}
events={data.events}
eventTypes={data.eventTypes}
options={timelineOptions}
annotations={annotations}
eventSelectionMode={true}
onTimelineDragEnd={onDragEndHandler}
onTimelineChange={(event) => {
if (event.eventSelection) {
setEventSelections(event.eventSelection);
}
if (event.annotations) {
setAnnotations(event.annotations);
}
if (event.range) {
hideAnnotationEditor();
}
}}
onTimelineClick={onClickEventHandler}
ref={timeline}
className="story__timeline"
/>
</div>
<div id="annotation-controls" className="story__controls story__controls--row">
<div className="story__controls__item">
<button
className="button"
type="button"
disabled={eventSelections.length === 0}
onClick={annotateSelection}
>
Annotate Selected
</button>
</div>
</div>
<div
id="annotation-editor"
ref={annotationEditor}
onKeyPress={function onKeyPress(event) {
if (event.key === "Enter") {
annotationEditorSaveElement.current?.click();
}
}}
>
<textarea id="annotation-editor-text" ref={annotationEditorText}></textarea>
<div className="annotation-editor-button-container">
<button
id="annotation-editor-save"
className="annotation-editor-button"
ref={annotationEditorSaveElement}
onClick={annotationEditorSave}
>
<i className="fas fa-check"></i>
</button>
<button
id="annotation-editor-cancel"
className="annotation-editor-button"
onClick={hideAnnotationEditor}
>
<i className="fas fa-times"></i>
</button>
</div>
</div>
</div>
);
};
const root = createRoot(document.getElementById("my-timeline"));
root.render(<Demo />); export default {
entities: {
"smith-johnathan-151": {
label: "John Smith",
},
"west-josephine-126": {
label: "Josephine West",
},
"roberts-nathaniel-023": {
label: "Nathan Roberts",
},
"baxter-eleanor-004": {
label: "Ella Baxter",
},
},
events: {
"Email 1": {
entityIds: ["smith-johnathan-151", "roberts-nathaniel-023"],
time: new Date(2025, 7, 14, 8, 49),
},
"Email 2": {
entityIds: ["smith-johnathan-151", "baxter-eleanor-004"],
time: new Date(2025, 7, 14, 8, 56),
},
"Email 3": {
entityIds: ["smith-johnathan-151", "west-josephine-126"],
time: new Date(2025, 7, 14, 9, 2),
},
"Email 4": {
entityIds: ["smith-johnathan-151", "west-josephine-126"],
time: new Date(2025, 7, 14, 9, 27),
},
"Email 5": {
entityIds: ["baxter-eleanor-004", "roberts-nathaniel-023"],
time: new Date(2025, 7, 14, 10, 20),
},
"Email 6": {
entityIds: ["roberts-nathaniel-023", "smith-johnathan-151"],
time: new Date(2025, 7, 14, 10, 30),
},
"Email 7": {
entityIds: ["west-josephine-126", "smith-johnathan-151"],
time: new Date(2025, 7, 14, 10, 40),
},
"Email 8": {
entityIds: ["smith-johnathan-151", "west-josephine-126"],
time: new Date(2025, 7, 14, 10, 51),
},
"Email 9": {
entityIds: ["west-josephine-126", "smith-johnathan-151"],
time: new Date(2025, 7, 14, 12, 5),
},
"Email 10": {
entityIds: ["smith-johnathan-151", "west-josephine-126"],
time: new Date(2025, 7, 14, 14, 37),
},
"Email 11": {
entityIds: ["baxter-eleanor-004", "west-josephine-126"],
time: new Date(2025, 7, 14, 16, 7),
},
"Email 12": {
entityIds: ["west-josephine-126", "roberts-nathaniel-023"],
time: new Date(2025, 7, 14, 17, 42),
},
},
}; <!doctype html>
<html>
<head>
<link rel="stylesheet" href="@ci/theme/kg/css/examples.css" />
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div id="my-timeline" style="height: 100vh"></div>
<script type="module" src="./code.jsx"></script>
</body>
</html> .timeline-wrapper,
.story__timeline {
height: 100%;
}
#annotation-editor {
background-color: #242c35;
display: none;
max-height: 300px;
max-width: 300px;
overflow-y: auto;
position: absolute;
border-radius: 8px;
}
#annotation-editor-text {
background-color: #242c35;
width: 100%;
color: #e5e8eb;
font-family: sans-serif;
font-size: 14px;
padding: 10px 10px 0 10px;
/* remove border on focus */
border: none;
overflow: auto;
outline: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
min-height: 58px;
resize: none; /*remove the resize handle on the bottom right*/
}
.annotation-editor-button {
background-color: #3e414b;
color: #afafaf;
border: none;
padding-block: 0;
padding-inline: 0;
height: 26px;
width: 26px;
margin: 5px;
}
.annotation-editor-button:hover {
background-color: #565a69;
}
.annotation-editor-button-container {
display: flex;
justify-content: right;
}
.switch {
position: relative;
display: inline-block;
width: 25px;
height: 16px;
padding: 2px;
vertical-align: middle;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ffffff;
-webkit-transition: 0.4s;
transition: 0.4s;
}
.slider:before {
position: absolute;
content: "";
height: 12px;
width: 12px;
left: 2px;
bottom: 2px;
background-color: #1f1f1f;
-webkit-transition: 0.4s;
transition: 0.4s;
}
input:checked + .slider {
background-color: #00a3ff;
}
input:focus + .slider {
box-shadow: 0 0 1px #00a3ff;
}
input:checked + .slider:before {
-webkit-transform: translateX(9px);
-ms-transform: translateX(9px);
transform: translateX(9px);
}
/* Rounded sliders */
.slider.round {
border-radius: 99px;
}
.slider.round:before {
border-radius: 50%;
}
.slider-label {
padding-left: 0.5rem;
vertical-align: middle;
}