I want to make a scrollable tooltip in Chartjs because I have a lot of data to show.
I have made an external HTML tooltip referencing the official document, but I am not sure how to make it scrollable.
I have added max-height and overflow-y in tooltipEL style (like the code below), but it is not working.
const getOrCreateTooltip = (chart: any) => {
let tooltipEl = chart.canvas.parentNode.querySelector("div");
if (!tooltipEl) {
tooltipEl = document.createElement("div");
tooltipEl.style.background = "rgba(0, 0, 0, 0.7)";
tooltipEl.style.borderRadius = "3px";
tooltipEl.style.color = "white";
tooltipEl.style.opacity = 1;
tooltipEl.style.pointerEvents = "none";
tooltipEl.style.position = "absolute";
tooltipEl.style.transform = "translate(-50%, 0)";
tooltipEl.style.transition = "all .1s ease";
const table = document.createElement("table");
table.style.margin = "0px";
tooltipEl.appendChild(table);
chart.canvas.parentNode.appendChild(tooltipEl);
}
return tooltipEl;
};
const externalTooltipHandler = (context: any) => {
// Tooltip Element
const { chart, tooltip } = context;
const tooltipEl = getOrCreateTooltip(chart);
// Hide if no tooltip
if (tooltip.opacity === 0) {
tooltipEl.style.opacity = 0;
return;
}
// Set Text
if (tooltip.body) {
const titleLines = tooltip.title || [];
const bodyLines = tooltip.body.map((b: any) => b.lines);
const tableHead = document.createElement("thead");
titleLines.forEach((title: any) => {
const tr = document.createElement("tr");
tr.style.borderWidth = "0";
const th = document.createElement("th");
th.style.borderWidth = "0";
const text = document.createTextNode(title);
th.appendChild(text);
tr.appendChild(th);
tableHead.appendChild(tr);
});
const tableBody = document.createElement("tbody");
bodyLines.forEach((body: any, i: any) => {
const colors = tooltip.labelColors[i];
const span = document.createElement("span");
span.style.background = colors.backgroundColor;
span.style.borderColor = colors.borderColor;
span.style.borderWidth = "2px";
span.style.marginRight = "10px";
span.style.height = "10px";
span.style.width = "10px";
span.style.display = "inline-block";
const tr = document.createElement("tr");
tr.style.fontSize = "14px";
tr.style.backgroundColor = "inherit";
tr.style.borderWidth = "0";
const td = document.createElement("td");
td.style.borderWidth = "0";
const text = document.createTextNode(body);
td.appendChild(span);
td.appendChild(text);
tr.appendChild(td);
tableBody.appendChild(tr);
});
const tableRoot = tooltipEl.querySelector("table");
while (tableRoot.firstChild) {
tableRoot.firstChild.remove();
}
tableRoot.appendChild(tableHead);
tableRoot.appendChild(tableBody);
}
const { offsetLeft: positionX, offsetTop: positionY } = chart.canvas;
tooltipEl.style.opacity = 1;
tooltipEl.style.left = positionX + tooltip.caretX + "px";
tooltipEl.style.top = positionY + tooltip.caretY + "px";
tooltipEl.style.font = tooltip.options.bodyFont.string;
tooltipEl.style.padding =
tooltip.options.padding + "px " + tooltip.options.padding + "px";
tooltipEl.style.maxHeight = "330px";
tooltipEl.style.overflowY = "auto";
tooltipEl.style.zIndex = 1000;
};
const options = {
maintainAspectRatio: false,
scales: {
x: {
offset: true,
grid: {
drawTicks: false,
offset: true,
color: "#00001A26",
},
ticks: {
color: "rgba(0 ,0 ,0 ,0.7)",
padding: 4,
stepSize: 1,
font: {
size: 12,
},
},
border: {
dash: [2, 2],
},
min: -3,
max: 3,
},
y: {
beginAtZero: true,
grid: {
drawTicks: false,
color: "#00001A26",
},
ticks: {
color: "rgba(0 ,0 ,0 ,0.7)",
padding: 4,
maxTicksLimit: 5,
font: {
size: 12,
},
},
border: {
dash: [2, 2],
display: false,
},
},
},
plugins: {
tooltip: {
enabled: false,
external: externalTooltipHandler,
},
},
};
Is it impossible to make the tooltip vertically scrollable?
I want to make a scrollable tooltip in Chartjs because I have a lot of data to show.
I have made an external HTML tooltip referencing the official document, but I am not sure how to make it scrollable.
I have added max-height and overflow-y in tooltipEL style (like the code below), but it is not working.
const getOrCreateTooltip = (chart: any) => {
let tooltipEl = chart.canvas.parentNode.querySelector("div");
if (!tooltipEl) {
tooltipEl = document.createElement("div");
tooltipEl.style.background = "rgba(0, 0, 0, 0.7)";
tooltipEl.style.borderRadius = "3px";
tooltipEl.style.color = "white";
tooltipEl.style.opacity = 1;
tooltipEl.style.pointerEvents = "none";
tooltipEl.style.position = "absolute";
tooltipEl.style.transform = "translate(-50%, 0)";
tooltipEl.style.transition = "all .1s ease";
const table = document.createElement("table");
table.style.margin = "0px";
tooltipEl.appendChild(table);
chart.canvas.parentNode.appendChild(tooltipEl);
}
return tooltipEl;
};
const externalTooltipHandler = (context: any) => {
// Tooltip Element
const { chart, tooltip } = context;
const tooltipEl = getOrCreateTooltip(chart);
// Hide if no tooltip
if (tooltip.opacity === 0) {
tooltipEl.style.opacity = 0;
return;
}
// Set Text
if (tooltip.body) {
const titleLines = tooltip.title || [];
const bodyLines = tooltip.body.map((b: any) => b.lines);
const tableHead = document.createElement("thead");
titleLines.forEach((title: any) => {
const tr = document.createElement("tr");
tr.style.borderWidth = "0";
const th = document.createElement("th");
th.style.borderWidth = "0";
const text = document.createTextNode(title);
th.appendChild(text);
tr.appendChild(th);
tableHead.appendChild(tr);
});
const tableBody = document.createElement("tbody");
bodyLines.forEach((body: any, i: any) => {
const colors = tooltip.labelColors[i];
const span = document.createElement("span");
span.style.background = colors.backgroundColor;
span.style.borderColor = colors.borderColor;
span.style.borderWidth = "2px";
span.style.marginRight = "10px";
span.style.height = "10px";
span.style.width = "10px";
span.style.display = "inline-block";
const tr = document.createElement("tr");
tr.style.fontSize = "14px";
tr.style.backgroundColor = "inherit";
tr.style.borderWidth = "0";
const td = document.createElement("td");
td.style.borderWidth = "0";
const text = document.createTextNode(body);
td.appendChild(span);
td.appendChild(text);
tr.appendChild(td);
tableBody.appendChild(tr);
});
const tableRoot = tooltipEl.querySelector("table");
while (tableRoot.firstChild) {
tableRoot.firstChild.remove();
}
tableRoot.appendChild(tableHead);
tableRoot.appendChild(tableBody);
}
const { offsetLeft: positionX, offsetTop: positionY } = chart.canvas;
tooltipEl.style.opacity = 1;
tooltipEl.style.left = positionX + tooltip.caretX + "px";
tooltipEl.style.top = positionY + tooltip.caretY + "px";
tooltipEl.style.font = tooltip.options.bodyFont.string;
tooltipEl.style.padding =
tooltip.options.padding + "px " + tooltip.options.padding + "px";
tooltipEl.style.maxHeight = "330px";
tooltipEl.style.overflowY = "auto";
tooltipEl.style.zIndex = 1000;
};
const options = {
maintainAspectRatio: false,
scales: {
x: {
offset: true,
grid: {
drawTicks: false,
offset: true,
color: "#00001A26",
},
ticks: {
color: "rgba(0 ,0 ,0 ,0.7)",
padding: 4,
stepSize: 1,
font: {
size: 12,
},
},
border: {
dash: [2, 2],
},
min: -3,
max: 3,
},
y: {
beginAtZero: true,
grid: {
drawTicks: false,
color: "#00001A26",
},
ticks: {
color: "rgba(0 ,0 ,0 ,0.7)",
padding: 4,
maxTicksLimit: 5,
font: {
size: 12,
},
},
border: {
dash: [2, 2],
display: false,
},
},
},
plugins: {
tooltip: {
enabled: false,
external: externalTooltipHandler,
},
},
};
Is it impossible to make the tooltip vertically scrollable?
Share Improve this question edited Mar 19 at 1:10 Ji Yeon Kim asked Mar 19 at 1:10 Ji Yeon KimJi Yeon Kim 111 bronze badge1 Answer
Reset to default 0Yes, it's possible. You can create a scrollable tooltip :
import React, { useEffect, useRef, useState } from "react";
const ScrollableTooltipDemo = () => {
const chartRef = useRef(null);
const containerRef = useRef(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Load Chart.js script dynamically
const loadChartJs = async () => {
try {
setLoading(true);
// Create script element to load Chart.js from CDN
const script = document.createElement("script");
script.src =
"https://cdnjs.cloudflare/ajax/libs/Chart.js/3.9.1/chart.min.js";
script.async = true;
// Set up load handler
script.onload = () => {
// Initialize chart once Chart.js is loaded
initializeChart();
setLoading(false);
};
// Set up error handler
script.onerror = () => {
setError("Failed to load Chart.js library");
setLoading(false);
};
// Add script to document
document.body.appendChild(script);
// Cleanup function
return () => {
document.body.removeChild(script);
};
} catch (err) {
setError(`Error loading Chart.js: ${err.message}`);
setLoading(false);
}
};
const initializeChart = () => {
// Ensure Chart.js is available
if (typeof window.Chart === "undefined") {
setError("Chart.js failed to load properly");
return;
}
const ctx = chartRef.current.getContext("2d");
// Generate sample data
const months = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
const generateRandomData = (count, min, max) =>
Array.from({ length: count }, () =>
Math.floor(Math.random() * (max - min + 1) + min)
);
// Define tooltip plugin
const tooltipPlugin = {
id: "tooltipInteraction",
beforeEvent(chart, args) {
const event = args.event;
if (event.type === "mouseout") {
// Check if we're still in the tooltip
const tooltipEl = chart.canvas.parentNode.querySelector(
"div.chartjs-tooltip"
);
if (tooltipEl && tooltipEl._isHovered) {
// Prevent default mouseout behavior
args.replay = false;
}
}
},
};
// External tooltip handler
const getOrCreateTooltip = (chart) => {
let tooltipEl = chart.canvas.parentNode.querySelector(
"div.chartjs-tooltip"
);
if (!tooltipEl) {
tooltipEl = document.createElement("div");
tooltipEl.className = "chartjs-tooltip";
tooltipEl.style.background = "rgba(0, 0, 0, 0.7)";
tooltipEl.style.borderRadius = "3px";
tooltipEl.style.color = "white";
tooltipEl.style.opacity = 1;
tooltipEl.style.pointerEvents = "auto"; // Enable pointer events
tooltipEl.style.position = "absolute";
tooltipEl.style.transform = "translate(-50%, 0)";
tooltipEl.style.transition = "all .1s ease";
tooltipEl.style.maxHeight = "150px"; // Set a reasonable max height
tooltipEl.style.overflowY = "auto"; // Enable vertical scrolling
tooltipEl.style.width = "220px"; // Fixed width for better scrolling
tooltipEl.style.zIndex = 1000;
tooltipEl.style.boxShadow = "0 2px 5px rgba(0,0,0,0.25)";
tooltipEl.style.padding = "10px";
const table = document.createElement("table");
table.style.margin = "0px";
table.style.width = "100%";
tooltipEl.appendChild(table);
chart.canvas.parentNode.appendChild(tooltipEl);
// Add event listeners for tooltip interaction
tooltipEl.addEventListener("mouseenter", () => {
tooltipEl._isHovered = true;
});
tooltipEl.addEventListener("mouseleave", () => {
tooltipEl._isHovered = false;
// Hide the tooltip when mouse leaves (with delay)
setTimeout(() => {
if (!tooltipEl._isHovered) {
tooltipEl.style.opacity = 0;
}
}, 500);
});
}
return tooltipEl;
};
const externalTooltipHandler = (context) => {
const { chart, tooltip } = context;
const tooltipEl = getOrCreateTooltip(chart);
// Check if we should hide the tooltip
if (tooltip.opacity === 0 && !tooltipEl._isHovered) {
tooltipEl.style.opacity = 0;
return;
}
// Set Text
if (tooltip.body) {
const titleLines = tooltip.title || [];
const bodyLines = tooltip.body.map((b) => b.lines);
const tableHead = document.createElement("thead");
titleLines.forEach((title) => {
const tr = document.createElement("tr");
tr.style.borderWidth = "0";
const th = document.createElement("th");
th.style.borderWidth = "0";
th.style.padding = "5px";
const text = document.createTextNode(title);
th.appendChild(text);
tr.appendChild(th);
tableHead.appendChild(tr);
});
const tableBody = document.createElement("tbody");
bodyLines.forEach((body, i) => {
const colors = tooltip.labelColors[i];
const span = document.createElement("span");
span.style.background = colors.backgroundColor;
span.style.borderColor = colors.borderColor;
span.style.borderWidth = "2px";
span.style.marginRight = "10px";
span.style.height = "10px";
span.style.width = "10px";
span.style.display = "inline-block";
const tr = document.createElement("tr");
tr.style.fontSize = "14px";
tr.style.backgroundColor = "inherit";
tr.style.borderWidth = "0";
const td = document.createElement("td");
td.style.borderWidth = "0";
td.style.padding = "5px";
const text = document.createTextNode(body);
td.appendChild(span);
td.appendChild(text);
tr.appendChild(td);
tableBody.appendChild(tr);
});
const tableRoot = tooltipEl.querySelector("table");
// Clear previous content
while (tableRoot.firstChild) {
tableRoot.firstChild.remove();
}
tableRoot.appendChild(tableHead);
tableRoot.appendChild(tableBody);
}
// Position the tooltip
const { offsetLeft: positionX, offsetTop: positionY } = chart.canvas;
tooltipEl.style.opacity = 1;
// Position adjustments to avoid tooltip being cut off
const tooltipWidth = tooltipEl.offsetWidth || 220;
const horizontalAdjustment =
tooltip.caretX + tooltipWidth / 2 > chart.width
? -(tooltip.caretX + tooltipWidth / 2 - chart.width)
: 0;
tooltipEl.style.left =
positionX + tooltip.caretX + horizontalAdjustment + "px";
tooltipEl.style.top = positionY + tooltip.caretY + "px";
tooltipEl.style.font = tooltip.options.bodyFont.string;
};
// Create datasets with multiple series
const data = {
labels: months,
datasets: [
{
label: "Dataset 1",
data: generateRandomData(12, 10, 100),
borderColor: "rgb(75, 192, 192)",
backgroundColor: "rgba(75, 192, 192, 0.2)",
tension: 0.3,
},
{
label: "Dataset 2",
data: generateRandomData(12, 20, 90),
borderColor: "rgb(255, 99, 132)",
backgroundColor: "rgba(255, 99, 132, 0.2)",
tension: 0.3,
},
{
label: "Dataset 3",
data: generateRandomData(12, 30, 80),
borderColor: "rgb(54, 162, 235)",
backgroundColor: "rgba(54, 162, 235, 0.2)",
tension: 0.3,
},
{
label: "Dataset 4",
data: generateRandomData(12, 5, 70),
borderColor: "rgb(255, 159, 64)",
backgroundColor: "rgba(255, 159, 64, 0.2)",
tension: 0.3,
},
{
label: "Dataset 5",
data: generateRandomData(12, 15, 85),
borderColor: "rgb(153, 102, 255)",
backgroundColor: "rgba(153, 102, 255, 0.2)",
tension: 0.3,
},
],
};
try {
// Register custom plugin
window.Chart.register(tooltipPlugin);
// Ensure canvas is correctly sized
const containerWidth = containerRef.current.offsetWidth;
chartRef.current.width = containerWidth;
chartRef.current.height = 300;
// Chart configuration
const chartInstance = new window.Chart(ctx, {
type: "line",
data: data,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "top",
},
title: {
display: true,
text: "Chart.js Line Chart with Scrollable Tooltip",
},
tooltip: {
enabled: false,
external: externalTooltipHandler,
mode: "index",
intersect: false,
events: [
"mousemove",
"mouseout",
"click",
"touchstart",
"touchmove",
],
},
},
scales: {
x: {
grid: {
drawTicks: false,
color: "#00001A26",
},
ticks: {
color: "rgba(0, 0, 0, 0.7)",
padding: 4,
font: {
size: 12,
},
},
border: {
dash: [2, 2],
},
},
y: {
beginAtZero: true,
grid: {
drawTicks: false,
color: "#00001A26",
},
ticks: {
color: "rgba(0, 0, 0, 0.7)",
padding: 4,
font: {
size: 12,
},
},
border: {
dash: [2, 2],
},
},
},
interaction: {
mode: "index",
intersect: false,
},
hover: {
mode: "index",
intersect: false,
},
},
});
// Store chart instance for cleanup
chartRef.current.chartInstance = chartInstance;
} catch (err) {
setError(`Error initializing chart: ${err.message}`);
}
};
loadChartJs();
// Cleanup on unmount
return () => {
if (chartRef.current && chartRef.current.chartInstance) {
chartRef.current.chartInstance.destroy();
}
};
}, []);
return (
<div className="w-full p-4">
<div className="flex flex-col items-center">
<div className="w-full max-w-4xl p-4 bg-white rounded-lg shadow-md">
<h2 className="text-xl font-bold mb-4 text-center">
Chart.js Scrollable Tooltip Demo
</h2>
<p className="mb-4 text-gray-700 text-center">
Hover over data points to see the scrollable tooltip. You can scroll
inside the tooltip without it disappearing.
</p>
<div
className="h-64 mb-4 relative"
ref={containerRef}
style={{ position: "relative", width: "100%", height: "300px" }}
>
{loading && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-gray-500">Loading chart...</div>
</div>
)}
{error && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-red-500">{error}</div>
</div>
)}
<canvas
ref={chartRef}
style={{ width: "100%", height: "100%" }}
></canvas>
</div>
<div className="text-sm text-gray-600 mt-4">
<p>
<strong>Instructions:</strong>
</p>
<ul className="list-disc pl-6">
<li>Hover over points on the chart to see the tooltip</li>
<li>
When the tooltip appears, you can move your cursor to it and
scroll
</li>
<li>The tooltip should remain visible while scrolling</li>
<li>
The tooltip will disappear after you move your cursor away
</li>
</ul>
</div>
</div>
</div>
</div>
);
};
export default ScrollableTooltipDemo;