Стандартный tooltip в Chart.js отрисовывается внутри canvas. Такой подход удобен для быстрых графиков, однако создаёт ограничения:
Механизм external позволяет полностью отключить
встроенный tooltip и заменить его собственной HTML-разметкой.
Главное преимущество — tooltip становится обычным DOM-элементом.
Chart.js предоставляет callback external, который
вызывается при каждом обновлении tooltip.
Схема работы:
Пользователь наводит курсор на график.
Chart.js вычисляет данные tooltip.
Вызывается функция external(context).
Разработчик самостоятельно:
Для начала необходимо отключить стандартный tooltip.
options: {
plugins: {
tooltip: {
enabled: false,
external: externalTooltipHandler
}
}
}
Свойства:
| Свойство | Назначение |
|---|---|
enabled: false |
отключает встроенный tooltip |
external |
подключает пользовательский обработчик |
Функция получает объект context.
function externalTooltipHandler(context) {
}
Внутри находятся:
const { chart, tooltip } = context;
| Объект | Назначение |
|---|---|
chart |
экземпляр графика |
tooltip |
текущее состояние tooltip |
<canvas id="myChart"></canvas>
<div id="chartjs-tooltip"></div>
const ctx = document.getElementById('myChart');
new Chart(ctx, {
type: 'line',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr'],
datasets: [{
label: 'Sales',
data: [12, 19, 7, 15],
borderColor: 'blue'
}]
},
options: {
plugins: {
tooltip: {
enabled: false,
external: externalTooltipHandler
}
}
}
});
function externalTooltipHandler(context) {
const { chart, tooltip } = context;
let tooltipEl = document.getElementById('chartjs-tooltip');
if (!tooltipEl) {
tooltipEl = document.createElement('div');
tooltipEl.id = 'chartjs-tooltip';
document.body.appendChild(tooltipEl);
}
if (tooltip.opacity === 0) {
tooltipEl.style.opacity = 0;
return;
}
if (tooltip.body) {
const title = tooltip.title[0];
const value = tooltip.body[0].lines[0];
tooltipEl.innerHTML = `
<div class="tooltip-title">${title}</div>
<div class="tooltip-value">${value}</div>
`;
}
const position = chart.canvas.getBoundingClientRect();
tooltipEl.style.opacity = 1;
tooltipEl.style.position = 'absolute';
tooltipEl.style.left =
position.left + window.pageXOffset + tooltip.caretX + 'px';
tooltipEl.style.top =
position.top + window.pageYOffset + tooltip.caretY + 'px';
}
Создавать элемент необходимо только один раз.
Плохой подход:
const div = document.createElement('div');
при каждом движении мыши.
Правильный подход:
let tooltipEl = document.getElementById('chartjs-tooltip');
if (!tooltipEl) {
tooltipEl = document.createElement('div');
}
Причины:
Когда курсор покидает точку графика:
tooltip.opacity === 0
необходимо скрыть HTML-элемент.
if (tooltip.opacity === 0) {
tooltipEl.style.opacity = 0;
return;
}
Иногда дополнительно используют:
tooltipEl.style.pointerEvents = 'none';
Это предотвращает перехват мыши самим tooltip.
tooltip.title
Обычно массив строк.
const title = tooltip.title[0];
tooltip.body
Содержит массив элементов.
const value = tooltip.body[0].lines[0];
tooltip.labelColors
Пример:
const colors = tooltip.labelColors[0];
console.log(colors.backgroundColor);
console.log(colors.borderColor);
Если график содержит несколько наборов данных:
datasets: [
{
label: '2024',
data: [10, 20, 30]
},
{
label: '2025',
data: [15, 25, 35]
}
]
tooltip может содержать несколько строк.
let innerHtml = '';
tooltip.body.forEach((bodyItem, index) => {
const colors = tooltip.labelColors[index];
innerHtml += `
<div class="tooltip-item">
<span
style="
background:${colors.backgroundColor};
width:10px;
height:10px;
display:inline-block;
"
></span>
${bodyItem.lines[0]}
</div>
`;
});
Chart.js предоставляет координаты:
| Свойство | Назначение |
|---|---|
tooltip.caretX |
X внутри canvas |
tooltip.caretY |
Y внутри canvas |
Но tooltip — HTML-элемент, поэтому требуется преобразование координат.
const position = chart.canvas.getBoundingClientRect();
tooltipEl.style.left =
position.left + window.pageXOffset + tooltip.caretX + 'px';
tooltipEl.style.top =
position.top + window.pageYOffset + tooltip.caretY + 'px';
Если страница прокручена:
window.pageYOffset
компенсирует scroll страницы.
Без этого tooltip будет смещаться.
Поскольку tooltip является обычным HTML:
#chartjs-tooltip {
background: #111;
color: white;
padding: 12px 16px;
border-radius: 8px;
font-family: Arial;
font-size: 14px;
pointer-events: none;
transition: all .15s ease;
transform: translate(-50%, -120%);
}
Без transform tooltip будет появляться:
Корректировка:
transform: translate(-50%, -120%);
даёт:
| Значение | Эффект |
|---|---|
-50% |
центрирование по X |
-120% |
смещение вверх |
#chartjs-tooltip::after {
content: '';
position: absolute;
left: 50%;
bottom: -6px;
transform: translateX(-50%);
border-width: 6px;
border-style: solid;
border-color: #111 transparent transparent transparent;
}
В отличие от стандартного tooltip можно вставлять полноценный HTML.
tooltipEl.innerHTML = `
<div class="tooltip-card">
<h4>${title}</h4>
<div class="tooltip-row">
<strong>Value:</strong>
${value}
</div>
<button>Details</button>
</div>
`;
tooltipEl.innerHTML = `
<div class="user-tooltip">
<img src="avatar.jpg">
<div>
<h3>${title}</h3>
<p>${value}</p>
</div>
</div>
`;
Наиболее полезная информация находится в:
tooltip.dataPoints
Пример:
const point = tooltip.dataPoints[0];
| Свойство | Назначение |
|---|---|
point.label |
label оси |
point.raw |
исходное значение |
point.dataset |
dataset |
point.dataIndex |
индекс точки |
point.datasetIndex |
индекс dataset |
const point = tooltip.dataPoints[0];
console.log(point.label);
console.log(point.raw);
console.log(point.dataset.label);
const point = tooltip.dataPoints[0];
tooltipEl.innerHTML = `
<div class="custom-tooltip">
<div class="tooltip-header">
${point.label}
</div>
<div class="tooltip-body">
<div>
Dataset:
${point.dataset.label}
</div>
<div>
Value:
${point.raw}
</div>
</div>
</div>
`;
let rows = '';
tooltip.dataPoints.forEach(point => {
rows += `
<tr>
<td>${point.dataset.label}</td>
<td>${point.raw}</td>
</tr>
`;
});
tooltipEl.innerHTML = `
<table>
<thead>
<tr>
<th>Dataset</th>
<th>Value</th>
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>
`;
#chartjs-tooltip {
opacity: 0;
transition:
opacity .2s ease,
transform .2s ease;
}
#chartjs-tooltip {
transform:
translate(-50%, -120%)
scale(.8);
}
#chartjs-tooltip.active {
transform:
translate(-50%, -120%)
scale(1);
}
tooltipEl.classList.add('active');
Частая проблема — tooltip уходит за границы окна браузера.
const rect = tooltipEl.getBoundingClientRect();
if (rect.right > window.innerWidth) {
tooltipEl.style.left =
window.innerWidth - rect.width - 20 + 'px';
}
if (rect.top < 0) {
tooltipEl.style.top = '20px';
}
Иногда график расположен внутри блока:
.chart-wrapper {
position: relative;
}
Тогда tooltip можно вставлять не в body, а в
контейнер.
chart.canvas.parentNode.appendChild(tooltipEl);
Это особенно полезно:
Tooltip можно динамически стилизовать.
tooltipEl.className = 'tooltip tooltip-visible';
или:
tooltipEl.classList.add('dark-theme');
.tooltip-dark {
background: #1e1e1e;
color: white;
}
.tooltip-light {
background: white;
color: black;
}
Хорошая практика — выносить создание tooltip в отдельные функции.
function getOrCreateTooltip(chart) {
let tooltipEl =
chart.canvas.parentNode.querySelector('div');
if (!tooltipEl) {
tooltipEl = document.createElement('div');
chart.canvas.parentNode.appendChild(tooltipEl);
}
return tooltipEl;
}
function externalTooltipHandler(context) {
const { chart, tooltip } = context;
const tooltipEl = getOrCreateTooltip(chart);
if (tooltip.opacity === 0) {
tooltipEl.style.opacity = 0;
return;
}
// update content
// update position
}
function renderTooltip(point) {
return `
<div class="tooltip">
<h4>${point.label}</h4>
<p>${point.raw}</p>
</div>
`;
}
external вызывается очень часто — практически при каждом
движении мыши.
Поэтому важно:
innerHTML;Если tooltip начинает мигать:
pointer-events: none;
обычно решает проблему.
Причина — tooltip начинает перехватывать курсор.
Для сложных tooltip иногда используют debounce.
let timer;
function updateTooltip() {
clearTimeout(timer);
timer = setTimeout(() => {
}, 16);
}
external хорошо подходит для интеграции с framework.
Вместо innerHTML:
setTooltipState({
x,
y,
value
});
Framework уже самостоятельно обновляет DOM.
tooltip: {
enabled: false,
external(context) {
const { tooltip } = context;
if (tooltip.opacity === 0) {
setVisible(false);
return;
}
setVisible(true);
setData({
x: tooltip.caretX,
y: tooltip.caretY,
points: tooltip.dataPoints
});
}
}
tooltipEl.innerHTML = `
<div
class="
bg-black
text-white
px-4
py-2
rounded-lg
shadow-lg
"
>
${value}
</div>
`;
Tooltip можно смещать вручную.
tooltipEl.style.left =
position.left +
tooltip.caretX +
20 +
'px';
Иногда tooltip должен отображаться строго над точкой.
transform: translate(-50%, -100%);
Лучше:
tooltipEl.style.opacity = 0;
чем:
tooltipEl.style.display = 'none';
Причины:
function getOrCreateTooltip(chart) {
let tooltipEl =
chart.canvas.parentNode.querySelector('.chart-tooltip');
if (!tooltipEl) {
tooltipEl = document.createElement('div');
tooltipEl.className = 'chart-tooltip';
chart.canvas.parentNode.appendChild(tooltipEl);
}
return tooltipEl;
}
function externalTooltipHandler(context) {
const { chart, tooltip } = context;
const tooltipEl = getOrCreateTooltip(chart);
if (tooltip.opacity === 0) {
tooltipEl.style.opacity = 0;
return;
}
const point = tooltip.dataPoints[0];
tooltipEl.innerHTML = `
<div class="tooltip-content">
<div class="tooltip-title">
${point.label}
</div>
<div class="tooltip-value">
${point.raw}
</div>
</div>
`;
const position =
chart.canvas.getBoundingClientRect();
tooltipEl.style.opacity = 1;
tooltipEl.style.position = 'absolute';
tooltipEl.style.left =
position.left +
window.pageXOffset +
tooltip.caretX +
'px';
tooltipEl.style.top =
position.top +
window.pageYOffset +
tooltip.caretY +
'px';
}
.chart-tooltip {
background: #111;
color: white;
padding: 10px 14px;
border-radius: 8px;
font-size: 14px;
pointer-events: none;
transform: translate(-50%, -120%);
transition: opacity .15s ease;
opacity: 0;
}