Экспорт Canvas в изображение

В Nivo визуализации могут отрисовываться двумя основными способами: через SVG и через Canvas. Canvas-вариант используется в высоконагруженных графиках, где требуется повышенная производительность при большом количестве точек, а также при частых обновлениях данных.

Компоненты вида ResponsiveLineCanvas, ResponsiveBarCanvas, ResponsiveScatterPlotCanvas заменяют SVG-деревья на единый <canvas> элемент, что напрямую влияет на подход к экспорту изображения.

Ключевая особенность Canvas-рендеринга:

  • отсутствует DOM-структура графических примитивов (линии, точки, оси);
  • итоговое изображение существует как пиксельный буфер;
  • экспорт выполняется через API Canvas (toDataURL, getImageData, toBlob).

Получение доступа к Canvas-элементу

Для экспорта необходимо получить ссылку на DOM-элемент <canvas>. В React это обычно делается через useRef.

import { ResponsiveLineCanvas } from '@nivo/line'
import { useRef } from 'react'

const Chart = ({ data }) => {
    const canvasRef = useRef(null)

    return (
        <div>
            <ResponsiveLineCanvas
                data={data}
                margin={{ top: 40, right: 40, bottom: 40, left: 40 }}
                ref={canvasRef}
            />
        </div>
    )
}

Однако важно учитывать, что не все обёртки Nivo напрямую пробрасывают ref на <canvas>. В таких случаях используется альтернативный подход через containerRef или поиск элемента в DOM:

const canvas = document.querySelector('canvas')

Именно этот элемент становится точкой экспорта.


Базовый экспорт Canvas в PNG

Основной механизм экспорта строится вокруг метода:

canvas.toDataURL('image/png')

Полный пример сохранения изображения:

function exportCanvasToPNG(canvas) {
    const image = canvas.toDataURL('image/png')

    const link = document.createElement('a')
    link.href = image
    link.download = 'chart.png'
    link.click()
}

При использовании в связке с Nivo:

const handleExport = () => {
    const canvas = document.querySelector('canvas')
    if (!canvas) return

    const image = canvas.toDataURL('image/png')

    const link = document.createElement('a')
    link.href = image
    link.download = 'nivo-chart.png'
    link.click()
}

Экспорт в JPEG с контролем качества

JPEG используется для уменьшения размера файла за счёт потерь качества.

const image = canvas.toDataURL('image/jpeg', 0.9)

Параметр качества:

  • 1.0 — максимальное качество
  • 0.7–0.9 — баланс
  • < 0.7 — заметные артефакты

Использование toBlob для больших графиков

Метод toBlob предпочтителен при работе с большими графиками, так как не перегружает память строковым base64.

function exportCanvasToImage(canvas) {
    canvas.toBlob((blob) => {
        const url = URL.createObjectURL(blob)

        const link = document.createElement('a')
        link.href = url
        link.download = 'chart.png'
        link.click()

        URL.revokeObjectURL(url)
    }, 'image/png')
}

Преимущества:

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

Масштабирование экспорта (retina/high DPI)

Canvas в браузере часто использует devicePixelRatio, что приводит к размытым экспортам при стандартном toDataURL.

Для повышения качества создаётся временный canvas с увеличенным разрешением:

function exportHiDPI(canvas) {
    const scale = window.devicePixelRatio || 1

    const exportCanvas = document.createElement('canvas')
    const ctx = exportCanvas.getContext('2d')

    exportCanvas.width = canvas.width * scale
    exportCanvas.height = canvas.height * scale

    ctx.scale(scale, scale)
    ctx.drawImage(canvas, 0, 0)

    const image = exportCanvas.toDataURL('image/png')

    const link = document.createElement('a')
    link.href = image
    link.download = 'chart-hidpi.png'
    link.click()
}

Экспорт сложных графиков Nivo с несколькими слоями

Canvas-версии Nivo часто используют внутренние слои:

  • оси;
  • сетка;
  • точки;
  • интерактивные оверлеи.

Некоторые из этих слоёв могут быть отрисованы отдельно или поверх основного canvas.

Проблема:

  • querySelector('canvas') может возвращать только базовый слой;
  • интерактивные элементы (tooltip, hover) не входят в финальный рендер.

Решение:

  • временно отключать hover-слои;
  • фиксировать состояние графика;
  • экспортировать только статичный canvas.

Пример логики:

const handleExport = () => {
    const canvas = document.querySelector('canvas')

    // предполагается, что tooltip отключён через state
    if (!canvas) return

    const image = canvas.toDataURL('image/png')
    download(image)
}

Экспорт SVG-режима как альтернатива Canvas

Хотя тема связана с Canvas, часто встречается гибридный подход: переключение на SVG ради экспорта.

SVG легче экспортировать через сериализацию:

const svg = document.querySelector('svg')
const serializer = new XMLSerializer()
const source = serializer.serializeToString(svg)

const blob = new Blob([source], { type: 'image/svg+xml;charset=utf-8' })

Далее SVG может быть конвертирован в PNG через:

  • canvas + drawImage
  • серверные библиотеки (например, Sharp)

Проблемы CORS при экспорте Canvas

Canvas становится “tainted”, если внутри графика используются внешние изображения без корректных CORS-заголовков.

Симптом:

  • toDataURL выбрасывает исключение безопасности.

Причины:

  • фоновые изображения;
  • иконки из внешних CDN;
  • кастомные image layers.

Решения:

  • использовать crossOrigin="anonymous" на изображениях;
  • хранить ассеты локально;
  • проверять CORS-заголовки сервера.

Экспорт дашборда с несколькими Nivo-графиками

При наличии нескольких canvas на странице задача усложняется: требуется объединение в одно изображение.

Подход:

function exportDashboard() {
    const canvases = document.querySelectorAll('canvas')

    const output = document.createElement('canvas')
    const ctx = output.getContext('2d')

    output.width = 1200
    output.height = 800

    let offsetY = 0

    canvases.forEach((c) => {
        ctx.drawImage(c, 0, offsetY)
        offsetY += c.height
    })

    const image = output.toDataURL('image/png')

    const link = document.createElement('a')
    link.href = image
    link.download = 'dashboard.png'
    link.click()
}

Использование библиотек для улучшенного экспорта

В сложных интерфейсах часто подключаются вспомогательные решения:

  • html-to-image — захват DOM-структуры;
  • html2canvas — рендер DOM в canvas;
  • dom-to-image-more — улучшенная версия с поддержкой сложных стилей.

Пример:

import { toPng } from 'html-to-image'

toPng(document.getElementById('chart-container'))
    .then((dataUrl) => {
        const link = document.createElement('a')
        link.download = 'chart.png'
        link.href = dataUrl
        link.click()
    })

Производительность при экспорте больших Canvas

При работе с Nivo Canvas на больших данных важно учитывать:

  • toDataURL блокирует main thread;
  • большие canvas могут вызывать лаги;
  • toBlob предпочтительнее;
  • экспорт лучше выполнять вне анимаций.

Практика оптимизации:

  • временная остановка анимаций;
  • фиксация данных;
  • отключение transition props в Nivo;
  • использование requestIdleCallback.

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

Для систем аналитики и отчётности часто применяется серверный рендеринг:

  • Puppeteer для захвата страницы;
  • Headless Chrome;
  • генерация PNG/PDF на сервере.

Пример логики:

  • открыть страницу с графиком;
  • дождаться загрузки данных;
  • сделать page.screenshot().

Этот подход устраняет ограничения браузера:

  • CORS;
  • память;
  • производительность клиента.

Особенности работы с прозрачностью

Canvas поддерживает альфа-канал, что важно при наложении графиков на дашборды.

const image = canvas.toDataURL('image/png')

PNG сохраняет прозрачность, JPEG — нет.

При наложении графиков на тёмные темы это критично:

  • PNG сохраняет фон контейнера;
  • JPEG приводит к заливке белым фоном.

Контроль качества итогового изображения

Факторы, влияющие на результат:

  • плотность пикселей (devicePixelRatio);
  • размеры контейнера;
  • тип шрифтов;
  • сглаживание линий (anti-aliasing);
  • качество исходных данных.

При необходимости высокой точности используется комбинированный подход:

  • увеличенный offscreen canvas;
  • ручной рендер;
  • фиксированные размеры графика.