Hover over events in the timeline to trigger the tooltip.
The hover event is triggered by hovering over items.
You can attach handlers to all pointer events using the timeline.on() function.
This story creates a simple HTML tooltip to show additional data.
See Also
Hover over events in the timeline to trigger the tooltip.
The onTimelineHover event is triggered by hovering over items.
This story creates a simple JSX tooltip to show additional data.
See Also
import { createTimeline } from "kronograph";
import data from "./data";
const tooltipElement = document.getElementById("my-tooltip");
const timelineElement = document.getElementById("my-timeline");
const timeline = createTimeline("my-timeline");
const showTooltip = ({ x, y, from, to }) => {
// display some email metadata in the tooltip
const bounds = timelineElement.getBoundingClientRect();
const left = bounds.left + x;
const top = bounds.top + y;
tooltipElement.style.display = "block";
// offset tooltip 10px from cursor
tooltipElement.style.left = `${left + 10}px`;
tooltipElement.style.top = `${top}px`;
const fromElement = document.getElementById("tooltip-from");
const toElement = document.getElementById("tooltip-to");
fromElement.innerText = from;
toElement.innerText = to;
};
function hideTooltip() {
tooltipElement.style.display = "none";
}
const handleHover = ({ eventIds, x, y }) => {
if (eventIds && eventIds.length > 0) {
const hoveredEvents = eventIds.map((eventId) => data.events[eventId]);
const { from: firstFrom, to: firstTo } = hoveredEvents[0].data;
const from = hoveredEvents.every((event) => event.data.from === firstFrom)
? firstFrom
: "Multiple entities";
const to = hoveredEvents.every((event) => event.data.to === firstTo)
? firstTo
: "Multiple entities";
showTooltip({ x, y, from, to });
} else {
hideTooltip();
}
};
timeline.options({ hover: 0 });
timeline.on("hover", handleHover);
timeline.on("range", hideTooltip);
timeline.set(data);
timeline.fit(); const data = {
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),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 2": {
entityIds: ["smith-johnathan-151", "baxter-eleanor-004"],
time: new Date(2025, 7, 14, 8, 56),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 3": {
entityIds: ["smith-johnathan-151", "west-josephine-126"],
time: new Date(2025, 7, 14, 9, 2),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 4": {
entityIds: ["smith-johnathan-151", "west-josephine-126"],
time: new Date(2025, 7, 14, 9, 27),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 5": {
entityIds: ["baxter-eleanor-004", "roberts-nathaniel-023"],
time: new Date(2025, 7, 14, 10, 20),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 6": {
entityIds: ["roberts-nathaniel-023", "smith-johnathan-151"],
time: new Date(2025, 7, 14, 10, 30),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 7": {
entityIds: ["smith-johnathan-151", "west-josephine-126"],
time: new Date(2025, 7, 14, 10, 34),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 8": {
entityIds: ["west-josephine-126", "smith-johnathan-151"],
time: new Date(2025, 7, 14, 10, 40),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 9": {
entityIds: ["smith-johnathan-151", "west-josephine-126"],
time: new Date(2025, 7, 14, 10, 51),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 10": {
entityIds: ["west-josephine-126", "smith-johnathan-151"],
time: new Date(2025, 7, 14, 12, 5),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 11": {
entityIds: ["smith-johnathan-151", "west-josephine-126"],
time: new Date(2025, 7, 14, 14, 37),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 12": {
entityIds: ["baxter-eleanor-004", "west-josephine-126"],
time: new Date(2025, 7, 14, 16, 7),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 13": {
entityIds: ["west-josephine-126", "roberts-nathaniel-023"],
time: new Date(2025, 7, 14, 17, 42),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
},
};
export default data; <!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="my-tooltip" class="story__tooltip stack stack--small">
<div>
<div class="story__tooltip__label">From</div>
<div class="story__tooltip__email" id="tooltip-from"></div>
</div>
<div>
<div class="story__tooltip__label">To</div>
<div class="story__tooltip__email" id="tooltip-to"></div>
</div>
</div>
</div>
<script type="module" src="./code.js"></script>
</body>
</html> @import url("https://fonts.googleapis.com/css?family=Muli:200,400,500,700&display=swap");
body {
font-family: "Muli", "Helvetica Neue", Helvetica, sans-serif;
font-weight: 400;
margin: 0;
}
.story {
background-color: hsl(210, 18%, 22%); /* Charcoal */
color: hsl(214, 15%, 91%);
display: flex;
flex-direction: column;
height: 100vh;
}
.story__timeline {
flex: 2;
min-height: 0px;
}
.story__tooltip {
background-color: hsl(209, 15%, 28%);
border-radius: 0.1975rem;
color: hsl(216, 33%, 97%);
display: none;
max-height: 300px;
max-width: 300px;
overflow-y: auto;
padding: 0.666rem;
position: absolute;
pointer-events: none;
}
.story__tooltip__label {
color: hsl(211, 13%, 65%);
font-size: 0.8em;
}
.story__tooltip__email {
color: hsl(214, 15%, 91%);
font-size: 0.9em;
}
.stack {
flex-direction: column;
justify-content: flex-start;
}
.stack > * {
margin-bottom: 0;
margin-top: 0;
}
.stack > * + * {
margin-top: 1rem;
}
.stack--small > * + * {
margin-top: 0.666rem;
} import React, { useState, useRef } from "react";
import { createRoot } from "react-dom/client";
import Timeline from "kronograph/react/Timeline";
import data from "./data";
const { events, entities } = data;
export const Demo = () => {
const storyContainer = useRef(null);
const [tooltip, setTooltip] = useState(null);
return (
<div className="story" ref={storyContainer}>
<Timeline
className="story__timeline"
entities={entities}
events={events}
options={{ hover: 0 }}
onTimelineHover={({ eventIds, x, y }) => {
if (eventIds && eventIds.length > 0) {
const hoveredEvents = eventIds.map((eventId) => data.events[eventId]);
const { from: firstFrom, to: firstTo } = hoveredEvents[0].data;
const from = hoveredEvents.every((event) => event.data.from === firstFrom)
? firstFrom
: "Multiple entities";
const to = hoveredEvents.every((event) => event.data.to === firstTo)
? firstTo
: "Multiple entities";
const bounds = storyContainer.current.getBoundingClientRect();
setTooltip({
from,
to,
// offset tooltip slightly from the cursor
left: bounds.left + 10 + x,
top: bounds.top + y,
});
} else {
setTooltip(null);
}
}}
onTimelineChange={(event) => {
if (event.range) {
setTooltip(null);
}
}}
/>
{tooltip ? (
<div
id="my-tooltip"
className="story__tooltip stack stack--small"
style={{
display: "block",
left: tooltip.left,
top: tooltip.top,
}}
>
<div>
<div className="story__tooltip__label">From</div>
<div className="story__tooltip__email" id="tooltip-from">
{tooltip.from}
</div>
</div>
<div>
<div className="story__tooltip__label">To</div>
<div className="story__tooltip__email" id="tooltip-to">
{tooltip.to}
</div>
</div>
</div>
) : null}
</div>
);
};
const root = createRoot(document.getElementById("my-timeline"));
root.render(<Demo />); const data = {
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),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 2": {
entityIds: ["smith-johnathan-151", "baxter-eleanor-004"],
time: new Date(2025, 7, 14, 8, 56),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 3": {
entityIds: ["smith-johnathan-151", "west-josephine-126"],
time: new Date(2025, 7, 14, 9, 2),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 4": {
entityIds: ["smith-johnathan-151", "west-josephine-126"],
time: new Date(2025, 7, 14, 9, 27),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 5": {
entityIds: ["baxter-eleanor-004", "roberts-nathaniel-023"],
time: new Date(2025, 7, 14, 10, 20),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 6": {
entityIds: ["roberts-nathaniel-023", "smith-johnathan-151"],
time: new Date(2025, 7, 14, 10, 30),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 7": {
entityIds: ["smith-johnathan-151", "west-josephine-126"],
time: new Date(2025, 7, 14, 10, 34),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 8": {
entityIds: ["west-josephine-126", "smith-johnathan-151"],
time: new Date(2025, 7, 14, 10, 40),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 9": {
entityIds: ["smith-johnathan-151", "west-josephine-126"],
time: new Date(2025, 7, 14, 10, 51),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 10": {
entityIds: ["west-josephine-126", "smith-johnathan-151"],
time: new Date(2025, 7, 14, 12, 5),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 11": {
entityIds: ["smith-johnathan-151", "west-josephine-126"],
time: new Date(2025, 7, 14, 14, 37),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 12": {
entityIds: ["baxter-eleanor-004", "west-josephine-126"],
time: new Date(2025, 7, 14, 16, 7),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
"email 13": {
entityIds: ["west-josephine-126", "roberts-nathaniel-023"],
time: new Date(2025, 7, 14, 17, 42),
data: {
from: "[email protected]",
to: "[email protected]",
},
},
},
};
export default data; <!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> @import url("https://fonts.googleapis.com/css?family=Muli:200,400,500,700&display=swap");
body {
font-family: "Muli", "Helvetica Neue", Helvetica, sans-serif;
font-weight: 400;
margin: 0;
}
.story {
background-color: hsl(210, 18%, 22%); /* Charcoal */
color: hsl(214, 15%, 91%);
display: flex;
flex-direction: column;
height: 100vh;
}
.story__timeline {
flex: 2;
min-height: 0px;
}
.story__tooltip {
background-color: hsl(209, 15%, 28%);
border-radius: 0.1975rem;
color: hsl(216, 33%, 97%);
display: none;
max-height: 300px;
max-width: 300px;
overflow-y: auto;
padding: 0.666rem;
position: absolute;
pointer-events: none;
}
.story__tooltip__label {
color: hsl(211, 13%, 65%);
font-size: 0.8em;
}
.story__tooltip__email {
color: hsl(214, 15%, 91%);
font-size: 0.9em;
}
.stack {
flex-direction: column;
justify-content: flex-start;
}
.stack > * {
margin-bottom: 0;
margin-top: 0;
}
.stack > * + * {
margin-top: 1rem;
}
.stack--small > * + * {
margin-top: 0.666rem;
}