Интеграция D3.js с Vue 3 строится вокруг разделения ответственности: Vue управляет состоянием и реактивностью, D3 отвечает за низкоуровневую отрисовку и манипуляции SVG/DOM. Основная архитектурная ошибка при работе с этими инструментами — попытка позволить им одновременно управлять одними и теми же DOM-узлами. Правильный подход заключается в том, что Vue предоставляет контейнер и данные, а D3 полностью контролирует содержимое внутри этого контейнера.
В Vue 3 доступ к реальному DOM-элементу осуществляется через
ref. Для D3 это ключевая точка входа: библиотека не должна
«искать» элементы через селекторы глобально, а должна работать строго
внутри выделенного узла.
<template>
<div ref="chartContainer"></div>
</template>
<script setup>
import { ref } from 'vue'
import * as d3 from 'd3'
const chartContainer = ref(null)
</script>
chartContainer остаётся пустым до момента монтирования
компонента. Любые операции D3 до этого этапа невозможны, так как
DOM-узел ещё не существует.
Жизненный цикл Vue требует инициализации D3-рендера в
onMounted. Это гарантирует наличие DOM и предотвращает
ошибки доступа к несуществующим элементам.
<script setup>
import { ref, onMounted } from 'vue'
import * as d3 from 'd3'
const chartContainer = ref(null)
onMounted(() => {
const width = 600
const height = 300
const svg = d3.select(chartContainer.value)
.append('svg')
.attr('width', width)
.attr('height', height)
svg.append('circle')
.attr('cx', 100)
.attr('cy', 100)
.attr('r', 40)
.attr('fill', 'steelblue')
})
</script>
На этом этапе создаётся базовая структура визуализации. D3 получает полный контроль над содержимым SVG внутри контейнера Vue.
Основная сложность интеграции — обновление графика при изменении
данных. Vue предоставляет реактивность, а watch
используется для отслеживания изменений и перерисовки визуализации.
<script setup>
import { ref, onMounted, watch } from 'vue'
import * as d3 from 'd3'
const chartContainer = ref(null)
const data = ref([10, 40, 25, 70, 50])
let svg
onMounted(() => {
svg = d3.select(chartContainer.value)
.append('svg')
.attr('width', 600)
.attr('height', 300)
renderChart(data.value)
})
watch(data, (newData) => {
renderChart(newData)
}, { deep: true })
function renderChart(dataset) {
const circles = svg.selectAll('circle')
.data(dataset)
circles
.enter()
.append('circle')
.merge(circles)
.attr('cx', (d, i) => i * 60 + 50)
.attr('cy', 150)
.attr('r', d => d / 2)
.attr('fill', 'teal')
circles.exit().remove()
}
</script>
Механизм watch позволяет синхронизировать реактивное
состояние Vue с императивной моделью D3. При изменении массива данных
запускается функция перерисовки.
Ключевая концепция D3 — data join: enter,
update, exit. В связке с Vue это становится
основным способом обновления визуализации без пересоздания SVG.
function renderChart(dataset) {
const selection = svg.selectAll('rect')
.data(dataset)
selection.enter()
.append('rect')
.merge(selection)
.attr('x', (d, i) => i * 50)
.attr('y', d => 300 - d)
.attr('width', 40)
.attr('height', d => d)
.attr('fill', 'orange')
selection.exit().remove()
}
Vue отвечает за изменение dataset, D3 — за минимально
необходимое обновление DOM.
Хотя watchEffect может автоматически отслеживать
зависимости, в связке с D3 предпочтительнее watch, так как
он даёт контроль над источником изменений и предотвращает лишние
перерисовки.
watch(data, (newVal, oldVal) => {
renderChart(newVal)
})
При использовании сложных структур данных важно учитывать глубокое наблюдение:
watch(data, renderChart, { deep: true })
Глубокий watch полезен, но может приводить к избыточным вызовам отрисовки при частых изменениях вложенных объектов.
При изменении размеров контейнера или смене набора данных иногда требуется полная очистка SVG. Это делается через D3 внутри реактивного контекста Vue.
function clearChart() {
d3.select(chartContainer.value).selectAll('*').remove()
}
Комбинация с watch позволяет полностью пересобирать
визуализацию при необходимости:
watch(data, (newData) => {
clearChart()
renderChart(newData)
})
Такой подход менее эффективен, но проще при сложной логике.
Корректная архитектура интеграции:
Vue:
D3:
Нарушение этого разделения приводит к конфликтам: Vue может перерисовать DOM, который D3 уже модифицировал, что вызывает нестабильное поведение.
Реактивные данные часто требуют пересчёта шкал D3:
const xScale = d3.scaleLinear().range([0, 600])
const yScale = d3.scaleLinear().range([300, 0])
watch(data, (dataset) => {
xScale.domain([0, dataset.length])
yScale.domain([0, d3.max(dataset)])
renderChart(dataset)
})
Шкалы не должны храниться в Vue, так как они являются частью визуализационной логики D3.
При наличии нескольких ref-источников данные
объединяются внутри watch:
watch([data, selectedIndex], ([newData, index]) => {
renderChart(newData, index)
})
Это позволяет синхронизировать визуализацию с интерфейсными состояниями Vue.
<script setup>
import { ref, onMounted, watch } from 'vue'
import * as d3 from 'd3'
const chartContainer = ref(null)
const data = ref([5, 15, 25, 10])
let svg
onMounted(() => {
svg = d3.select(chartContainer.value)
.append('svg')
.attr('width', 600)
.attr('height', 300)
renderChart(data.value)
})
watch(data, (val) => {
renderChart(val)
})
function renderChart(dataset) {
const bars = svg.selectAll('rect')
.data(dataset)
bars.enter()
.append('rect')
.merge(bars)
.attr('x', (d, i) => i * 50)
.attr('y', d => 300 - d * 10)
.attr('width', 40)
.attr('height', d => d * 10)
bars.exit().remove()
}
</script>
<template>
<div ref="chartContainer"></div>
</template>
Такая структура обеспечивает стабильную синхронизацию реактивного состояния Vue и императивного рендеринга D3 без конфликтов владения DOM.