По умолчанию графики в Chart.js экспортируются с прозрачным фоном.
Метод toBase64Image() сохраняет только содержимое canvas,
не добавляя цвет подложки. В результате:
Особенно заметна проблема при использовании тёмных тем, полупрозрачных элементов и сложных интерфейсов.
toBase64ImageСтандартный экспорт выглядит следующим образом:
const image = chart.toBase64Image();
Chart.js возвращает изображение содержимого canvas без фоновой заливки. Если canvas визуально расположен на белом блоке страницы, этот фон в изображение не попадёт.
Наиболее распространённый способ — вручную закрасить canvas перед сохранением.
const ctx = chart.ctx;
ctx.save();
ctx.globalCompositeOperation = 'destination-over';
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, chart.width, chart.height);
ctx.restore();
const image = chart.toBase64Image();
destination-overРежим:
globalCompositeOperation = 'destination-over'
позволяет рисовать новый слой под уже существующим содержимым canvas.
Без него белый прямоугольник перекроет график.
destination-over заливка помещается под
графиком.Ручная заливка неудобна при множественных графиках. Более надёжный способ — создание плагина.
const backgroundPlugin = {
id: 'customCanvasBackgroundColor',
beforeDraw: (chart, args, options) => {
const { ctx } = chart;
ctx.save();
ctx.globalCompositeOperation = 'destination-over';
ctx.fillStyle = options.color || '#ffffff';
ctx.fillRect(0, 0, chart.width, chart.height);
ctx.restore();
}
};
Chart.register(backgroundPlugin);
const chart = new Chart(ctx, {
type: 'bar',
data: {
labels: ['A', 'B', 'C'],
datasets: [{
label: 'Продажи',
data: [12, 19, 7]
}]
},
options: {
plugins: {
customCanvasBackgroundColor: {
color: '#ffffff'
}
}
},
plugins: [backgroundPlugin]
});
После регистрации плагина дополнительный код больше не требуется:
const image = chart.toBase64Image();
Фон автоматически попадёт в экспортируемое изображение.
Для тёмных интерфейсов:
plugins: {
customCanvasBackgroundColor: {
color: '#1e1e1e'
}
}
Часто цвет выбирается динамически:
const isDarkTheme = true;
const backgroundColor = isDarkTheme
? '#1e1e1e'
: '#ffffff';
Далее:
plugins: {
customCanvasBackgroundColor: {
color: backgroundColor
}
}
JPEG не поддерживает прозрачность. Без фоновой заливки изображение может экспортироваться с чёрным фоном.
const image = chart.toBase64Image('image/jpeg', 1);
Перед этим обязательно требуется закрашивание canvas.
Для печати и презентаций обычно увеличивают размер canvas.
const chart = new Chart(ctx, {
options: {
devicePixelRatio: 3
}
});
Чем выше devicePixelRatio, тем более чётким будет
экспорт.
beforeDrawПлагин чаще всего использует хук:
beforeDraw
Причины:
beforeDraw и afterDrawbeforeDrawФон располагается под графиком.
afterDrawФон перекроет график, если не использовать дополнительные режимы смешивания.
Вместо цвета можно использовать изображение.
const image = new Image();
image.src = 'background.png';
Плагин:
const imageBackgroundPlugin = {
id: 'imageBackground',
beforeDraw(chart) {
const { ctx } = chart;
ctx.save();
ctx.globalCompositeOperation = 'destination-over';
ctx.drawImage(
image,
0,
0,
chart.width,
chart.height
);
ctx.restore();
}
};
Если картинка не успела загрузиться:
drawImage()
ничего не нарисует.
Поэтому экспорт выполняют только после:
image.onl oad = () => {
chart.update();
};
Canvas позволяет создавать градиенты.
beforeDraw(chart) {
const { ctx, width, height } = chart;
const gradient = ctx.createLinearGradient(0, 0, 0, height);
gradient.addColorStop(0, '#4facfe');
gradient.addColorStop(1, '#00f2fe');
ctx.save();
ctx.globalCompositeOperation = 'destination-over';
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
ctx.restore();
}
const gradient = ctx.createRadialGradient(
width / 2,
height / 2,
50,
width / 2,
height / 2,
300
);
Canvas поддерживает текстурные фоны:
const pattern = ctx.createPattern(image, 'repeat');
Далее:
ctx.fillStyle = pattern;
Полупрозрачный фон:
ctx.fillStyle = 'rgba(255,255,255,0.8)';
Однако PNG снова станет частично прозрачным.
Для корректной печати рекомендуется использовать:
rgba(255,255,255,1)
или обычный HEX:
#ffffff
Иногда фон не нужен в интерфейсе, но необходим в итоговом изображении.
function exportChart(chart) {
const ctx = chart.ctx;
ctx.save();
ctx.globalCompositeOperation = 'destination-over';
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, chart.width, chart.height);
ctx.restore();
return chart.toBase64Image();
}
Если функция вызывается многократно, возможно накопление слоёв.
Чтобы избежать этого:
chart.draw();
или:
chart.update();
после экспорта.
Иногда экспорт выполняется через копирование содержимого.
const exportCanvas = document.createElement('canvas');
exportCanvas.width = chart.width;
exportCanvas.height = chart.height;
const exportCtx = exportCanvas.getContext('2d');
exportCtx.fillStyle = '#ffffff';
exportCtx.fillRect(
0,
0,
exportCanvas.width,
exportCanvas.height
);
exportCtx.drawImage(chart.canvas, 0, 0);
const image = exportCanvas.toDataURL('image/png');
Для Retina-экранов:
exportCanvas.width = chart.width * 2;
exportCanvas.height = chart.height * 2;
exportCtx.scale(2, 2);
const padding = 40;
Размеры:
exportCanvas.width = chart.width + padding * 2;
exportCanvas.height = chart.height + padding * 2;
Рисование:
exportCtx.drawImage(
chart.canvas,
padding,
padding
);
Часто экспортируемое изображение оформляют как карточку.
exportCtx.fillStyle = '#ffffff';
exportCtx.fillRect(0, 0, width, height);
exportCtx.strokeStyle = '#dddddd';
exportCtx.strokeRect(0, 0, width, height);
exportCtx.shadowColor = 'rgba(0,0,0,0.2)';
exportCtx.shadowBlur = 20;
Canvas не имеет встроенного border-radius, поэтому
используется путь:
ctx.beginPath();
function roundRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(
x + width,
y,
x + width,
y + radius
);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(
x + width,
y + height,
x + width - radius,
y + height
);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(
x,
y + height,
x,
y + height - radius
);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(
x,
y,
x + radius,
y
);
ctx.closePath();
}
roundRect(ctx, 0, 0, width, height, 20);
ctx.clip();
CSS:
canvas {
background: white;
}
не влияет на экспорт.
Причина в том, что:
background: white;
виден только визуально.
ctx.fillRect(...)
становится частью изображения.
Некоторые проекты экспортируют весь контейнер:
html2canvas(container)
В этом случае CSS-фон уже попадает в изображение.
Но возникают недостатки:
Для Chart.js оптимальной считается схема:
beforeDraw;destination-over;toBase64Image().Такой подход: