В Nivo визуализации могут отрисовываться двумя основными способами: через SVG и через Canvas. Canvas-вариант используется в высоконагруженных графиках, где требуется повышенная производительность при большом количестве точек, а также при частых обновлениях данных.
Компоненты вида ResponsiveLineCanvas,
ResponsiveBarCanvas,
ResponsiveScatterPlotCanvas заменяют SVG-деревья на
единый <canvas> элемент, что напрямую влияет на
подход к экспорту изображения.
Ключевая особенность Canvas-рендеринга:
toDataURL,
getImageData, toBlob).Для экспорта необходимо получить ссылку на 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.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 используется для уменьшения размера файла за счёт потерь качества.
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')
}
Преимущества:
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()
}
Canvas-версии Nivo часто используют внутренние слои:
Некоторые из этих слоёв могут быть отрисованы отдельно или поверх основного canvas.
Проблема:
querySelector('canvas') может возвращать только базовый
слой;Решение:
Пример логики:
const handleExport = () => {
const canvas = document.querySelector('canvas')
// предполагается, что tooltip отключён через state
if (!canvas) return
const image = canvas.toDataURL('image/png')
download(image)
}
Хотя тема связана с 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 + drawImageCanvas становится “tainted”, если внутри графика используются внешние изображения без корректных CORS-заголовков.
Симптом:
toDataURL выбрасывает исключение безопасности.Причины:
Решения:
crossOrigin="anonymous" на
изображениях;При наличии нескольких 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()
}
В сложных интерфейсах часто подключаются вспомогательные решения:
Пример:
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()
})
При работе с Nivo Canvas на больших данных важно учитывать:
toDataURL блокирует main thread;toBlob предпочтительнее;Практика оптимизации:
Для систем аналитики и отчётности часто применяется серверный рендеринг:
Пример логики:
page.screenshot().Этот подход устраняет ограничения браузера:
Canvas поддерживает альфа-канал, что важно при наложении графиков на дашборды.
const image = canvas.toDataURL('image/png')
PNG сохраняет прозрачность, JPEG — нет.
При наложении графиков на тёмные темы это критично:
Факторы, влияющие на результат:
devicePixelRatio);При необходимости высокой точности используется комбинированный подход: