Интеграция D3.js в Svelte требует понимания различий в подходах к работе с DOM. Svelte компилирует декларативный код в императивные операции обновления DOM, тогда как D3 напрямую манипулирует элементами через выборки и мутации.
Ключевой принцип интеграции — разделение зон ответственности:
Наиболее стабильный паттерн заключается в использовании D3 внутри
onMount, когда DOM уже гарантированно существует.
В Svelte onMount выполняется после первичного рендеринга
компонента, что делает его оптимальной точкой для инициализации
D3-графиков.
Основная задача на этом этапе:
bind:thisПример базовой структуры:
<script>
import { onMount } from 'svelte';
import * as d3 from 'd3';
let container;
let data = [10, 20, 30, 25, 15];
onMount(() => {
const width = 500;
const height = 300;
const svg = d3.select(container)
.append('svg')
.attr('width', width)
.attr('height', height);
const xScale = d3.scaleBand()
.domain(data.map((_, i) => i))
.range([0, width])
.padding(0.1);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data)])
.nice()
.range([height, 0]);
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', (_, i) => xScale(i))
.attr('y', d => yScale(d))
.attr('width', xScale.bandwidth())
.attr('height', d => height - yScale(d));
});
</script>
<div bind:this={container}></div>
Svelte и D3 по-разному подходят к обновлению интерфейса:
Из этого следует важное ограничение: не допускается одновременное управление одними и теми же DOM-элементами двумя системами.
Практическое правило:
В Svelte реактивные выражения позволяют отслеживать изменения данных.
Однако прямой перезапуск всей визуализации в onMount
недостаточен.
Используется паттерн обновления через функцию рендера:
<script>
import { onMount } from 'svelte';
import * as d3 from 'd3';
let container;
let data = [5, 12, 8, 22];
let svg, xScale, yScale;
function render() {
const width = 400;
const height = 200;
yScale.domain([0, d3.max(data)]);
const bars = svg.selectAll('rect')
.data(data);
bars.join(
enter => enter.append('rect')
.attr('x', (_, i) => xScale(i))
.attr('width', xScale.bandwidth())
.attr('y', height)
.attr('height', 0)
.call(enter => enter.transition()
.attr('y', d => yScale(d))
.attr('height', d => height - yScale(d))),
update => update
.call(update => update.transition()
.attr('y', d => yScale(d))
.attr('height', d => height - yScale(d))),
exit => exit
.call(exit => exit.transition()
.attr('y', height)
.attr('height', 0)
.remove())
);
}
onMount(() => {
const width = 400;
const height = 200;
svg = d3.select(container)
.append('svg')
.attr('width', width)
.attr('height', height);
xScale = d3.scaleBand()
.domain(d3.range(data.length))
.range([0, width])
.padding(0.1);
yScale = d3.scaleLinear()
.range([height, 0]);
render();
});
$: if (svg) {
render();
}
</script>
<div bind:this={container}></div>
Реактивные блоки Svelte ($:) позволяют синхронизировать
изменения данных с D3-рендерингом без ручного управления событиями.
Подход:
data автоматически вызывает
render()joinЭто минимизирует количество операций удаления и пересоздания элементов.
Для масштабируемых приложений D3-логика часто выносится в отдельный модуль. Это снижает связность с Svelte-компонентом.
// chart.js
import * as d3 from 'd3';
export function createChart(node, data) {
const width = 600;
const height = 300;
const svg = d3.select(node)
.append('svg')
.attr('width', width)
.attr('height', height);
const x = d3.scaleBand()
.domain(d3.range(data.length))
.range([0, width])
.padding(0.1);
const y = d3.scaleLinear()
.domain([0, d3.max(data)])
.range([height, 0]);
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', (_, i) => x(i))
.attr('y', d => y(d))
.attr('width', x.bandwidth())
.attr('height', d => height - y(d));
return {
update(newData) {
y.domain([0, d3.max(newData)]);
svg.selectAll('rect')
.data(newData)
.join('rect')
.transition()
.attr('y', d => y(d))
.attr('height', d => height - y(d));
}
};
}
Svelte-компонент:
<script>
import { onMount } from 'svelte';
import { createChart } from './chart.js';
let container;
let chart;
let data = [3, 7, 4, 9];
onMount(() => {
chart = createChart(container, data);
});
$: if (chart) {
chart.update(data);
}
</script>
<div bind:this={container}></div>
Альтернативный подход — использование Svelte actions, позволяющих привязать D3-логику к DOM-элементу.
// d3Action.js
import * as d3 from 'd3';
export function barChart(node, data) {
const svg = d3.select(node)
.append('svg')
.attr('width', 500)
.attr('height', 300);
function update(data) {
const y = d3.scaleLinear()
.domain([0, d3.max(data)])
.range([300, 0]);
svg.selectAll('rect')
.data(data)
.join('rect')
.attr('x', (_, i) => i * 40)
.attr('y', d => y(d))
.attr('width', 30)
.attr('height', d => 300 - y(d));
}
update(data);
return {
update,
destroy() {
svg.remove();
}
};
}
Использование:
<script>
import { barChart } from './d3Action.js';
let data = [2, 6, 1, 8];
</script>
<div use:barChart={data}></div>
D3 создаёт DOM-узлы и слушатели событий, которые не удаляются автоматически Svelte.
При использовании onMount или actions необходимо
учитывать:
Стандартный механизм:
onMount(() => {
const svg = d3.select(container).append('svg');
return () => {
svg.remove();
};
});
D3 transitions могут конфликтовать с реактивными обновлениями Svelte при частом изменении данных.
Стабильный подход:
render/updateПример:
selection.transition()
.duration(750)
.attr('y', d => y(d))
.attr('height', d => height - y(d));
При усложнении интерфейса формируется структура:
Такая модель позволяет избегать дублирования ответственности и снижает риск конфликтов между реактивной системой и императивным рендерингом.