Настройка фона перед экспортом

По умолчанию графики в Chart.js экспортируются с прозрачным фоном. Метод toBase64Image() сохраняет только содержимое canvas, не добавляя цвет подложки. В результате:

  • PNG может выглядеть корректно только на белых страницах;
  • при вставке в презентации появляются тёмные области;
  • PDF-документы отображают прозрачность некорректно;
  • изображения теряют визуальную целостность при печати.

Особенно заметна проблема при использовании тёмных тем, полупрозрачных элементов и сложных интерфейсов.


Экспорт изображения через 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.

Без него белый прямоугольник перекроет график.

Схема работы

  1. Сначала Chart.js рисует график.
  2. Затем выполняется заливка.
  3. Благодаря destination-over заливка помещается под графиком.
  4. Canvas экспортируется вместе с фоном.

Использование пользовательского плагина

Ручная заливка неудобна при множественных графиках. Более надёжный способ — создание плагина.


Плагин фоновой заливки

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

JPEG не поддерживает прозрачность. Без фоновой заливки изображение может экспортироваться с чёрным фоном.

Правильный экспорт

const image = chart.toBase64Image('image/jpeg', 1);

Перед этим обязательно требуется закрашивание canvas.


Экспорт высокого качества

Для печати и презентаций обычно увеличивают размер canvas.

Пример

const chart = new Chart(ctx, {
    options: {
        devicePixelRatio: 3
    }
});

Чем выше devicePixelRatio, тем более чётким будет экспорт.


Использование beforeDraw

Плагин чаще всего использует хук:

beforeDraw

Причины:

  • график ещё не полностью отрисован;
  • фон рисуется раньше остальных элементов;
  • уменьшается вероятность артефактов.

Разница между beforeDraw и afterDraw

beforeDraw

Фон располагается под графиком.

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

Фон только во время экспорта

Иногда фон не нужен в интерфейсе, но необходим в итоговом изображении.

Подход

  1. Добавить фон.
  2. Выполнить экспорт.
  3. Перерисовать график.

Пример временной заливки

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();

после экспорта.


Использование отдельного canvas

Иногда экспорт выполняется через копирование содержимого.

Алгоритм

  1. Создаётся временный canvas.
  2. На него переносится график.
  3. Добавляется фон.
  4. Выполняется экспорт.

Пример копирования

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');

Преимущества отдельного canvas

  • не изменяется оригинальный график;
  • отсутствуют артефакты;
  • удобно для пакетного экспорта;
  • можно применять собственные размеры;
  • проще масштабирование.

Масштабирование перед экспортом

Для 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-фона

CSS:

canvas {
    background: white;
}

не влияет на экспорт.

Причина в том, что:

  • CSS оформляет DOM-элемент;
  • экспортируется только внутреннее содержимое canvas;
  • браузер не объединяет CSS-фон с bitmap-данными canvas.

Отличие DOM-фона и canvas-фона

DOM-фон

background: white;

виден только визуально.

Canvas-фон

ctx.fillRect(...)

становится частью изображения.


Использование HTML2Canvas

Некоторые проекты экспортируют весь контейнер:

html2canvas(container)

В этом случае CSS-фон уже попадает в изображение.

Но возникают недостатки:

  • снижение качества;
  • проблемы со шрифтами;
  • медленный рендер;
  • сложность масштабирования;
  • зависимость от DOM.

Наиболее надёжный способ

Для Chart.js оптимальной считается схема:

  1. пользовательский plugin;
  2. beforeDraw;
  3. destination-over;
  4. непрозрачная заливка;
  5. экспорт через toBase64Image().

Такой подход:

  • совместим со всеми типами графиков;
  • не зависит от CSS;
  • работает в PNG и JPEG;
  • поддерживает тёмные темы;
  • подходит для серверного рендеринга;
  • легко масштабируется под любые задачи экспорта.