Интеграция D3 с Svelte: использование D3 в onMount

Интеграция D3.js в Svelte требует понимания различий в подходах к работе с DOM. Svelte компилирует декларативный код в императивные операции обновления DOM, тогда как D3 напрямую манипулирует элементами через выборки и мутации.

Ключевой принцип интеграции — разделение зон ответственности:

  • Svelte управляет жизненным циклом компонентов и реактивными данными
  • D3 управляет SVG/Canvas-рендерингом и визуальными трансформациями

Наиболее стабильный паттерн заключается в использовании D3 внутри onMount, когда DOM уже гарантированно существует.


Жизненный цикл onMount как точка инициализации визуализации

В Svelte onMount выполняется после первичного рендеринга компонента, что делает его оптимальной точкой для инициализации D3-графиков.

Основная задача на этом этапе:

  • получить ссылку на DOM-элемент через bind:this
  • создать SVG-контейнер
  • инициализировать оси, шкалы и базовые элементы визуализации

Пример базовой структуры:

<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>

Конфликт моделей обновления DOM

Svelte и D3 по-разному подходят к обновлению интерфейса:

  • Svelte пересоздаёт DOM на основе реактивных переменных
  • D3 изменяет DOM-узлы напрямую через selection API

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

Практическое правило:

  • контейнер, созданный Svelte, становится «чёрным ящиком» для D3
  • внутри этого контейнера Svelte не должен рендерить разметку, которой управляет D3

Реактивные данные и повторный рендеринг D3

В 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 реактивности вместо ручных обновлений

Реактивные блоки Svelte ($:) позволяют синхронизировать изменения данных с D3-рендерингом без ручного управления событиями.

Подход:

  • изменение data автоматически вызывает render()
  • D3 обновляет только изменённые части DOM через join

Это минимизирует количество операций удаления и пересоздания элементов.


Инкапсуляция D3-графика в отдельную функцию

Для масштабируемых приложений 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 как механизм интеграции

Альтернативный подход — использование 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 необходимо учитывать:

  • удаление SVG при уничтожении компонента
  • очистку таймеров анимации
  • удаление event listeners

Стандартный механизм:

onMount(() => {
  const svg = d3.select(container).append('svg');

  return () => {
    svg.remove();
  };
});

Анимации и переходы в связке Svelte + D3

D3 transitions могут конфликтовать с реактивными обновлениями Svelte при частом изменении данных.

Стабильный подход:

  • использовать D3 transitions только внутри render/update
  • избегать параллельных Svelte-анимаций на тех же элементах
  • централизовать управление временем анимаций внутри D3

Пример:

selection.transition()
  .duration(750)
  .attr('y', d => y(d))
  .attr('height', d => height - y(d));

Масштабируемая архитектура визуализаций

При усложнении интерфейса формируется структура:

  • Svelte: состояние приложения, фильтры, загрузка данных
  • D3: визуальные представления, расчёт геометрии, отрисовка
  • отдельные chart-модули: инкапсуляция логики визуализации
  • actions: связывание DOM и D3 без вмешательства в компонент

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