Интеграция визуализации данных D3 во внешние приложения может быть сложной задачей, когда жизненный цикл графика контролируется приложением, иногда может быть неясно, как правильно связать график и приложение. В этой статье я описываю несколько способов обновления графиков с сохранением их внутреннего состояния.

График внутреннего состояния

При создании графика D3 для визуализации некоторых данных часто бывает так, что для улучшения взаимодействия с пользователем необходимо добавить некоторую интерактивность. А с интерактивностью обычно приходит состояние, и вместе с этим сложность жизненного цикла сюжета может быстро возрастать. В состоянии графика может быть сохранено много разных вещей, таких как положение прокрутки, выбранные элементы, значение масштабирования и т. д. В основном, это может быть любой эффект возможного пользовательского ввода, не связанный напрямую с изменениями данных.

Стандартный жизненный цикл графика D3 (как и большинства других библиотек) выглядит так:

Первоначальный рендер

  • применение данных
  • визуализация сюжетных компонентов

Обновить

  • применение новых данных
  • перерисовка сюжетных компонентов

Уничтожение

  • очистка отображаемых компонентов и любых других возможных подключений (например, подписки на события и т. д.)

Чтобы добиться сохранения внутреннего состояния сюжета, жизненный цикл должен быть реализован каким-то определенным образом.

Перерисовать сюжет с нуля

Для простого графика, который представляет некоторые данные без обработки больших данных или ожидания ввода данных пользователем, шаг уничтожения можно использовать для перерисовки всей визуализации вместо разделения логики обновления. Это означает, что график должен быть уничтожен и заново визуализирован с нуля при любом обновлении данных. Такой подход помогает упростить логику рендеринга графика, но также очищает любое состояние, которое может храниться в графике.

function drawExample1() {
  const plot = d3.select('#example1')

  plot.selectAll('*').remove()

  // ...
}

На снимках экрана ниже показано, что масштабирование и положение прокрутки будут сброшены после любого обновления данных и перерисовки графика

Чтобы избежать очистки состояния после каждого обновления, при изменении данных должна выполняться только определенная часть функции графика. Есть несколько способов, как этого можно добиться.

Запустить логику инициализации состояния при первом рендеринге

Этот способ лучше всего работает, если график не содержит никакого состояния, которое влияет на данные или связано с самими данными (например, выбор элементов, пользовательский ввод и т. д.), но имеет некоторое состояние представления графика (масштабирование, прокрутка, позиционирование элементов и т. д.). как это).

Чтобы избежать извлечения логики обновления и увеличения сложности, можно использовать условие для выполнения части функции графика с инициализацией некоторых функций с отслеживанием состояния только при первом запуске (когда график отображается изначально).

Конкретное условие может быть разным, это зависит от реализации сюжета. Например, для этого можно использовать проверку корневого контейнера (например, какого-либо элемента g или самого элемента svg) на пустоту.

function drawExample2() {
  const plot = d3.select('#example2')

  const r = 40
  const nodeSeparation = 50

  const nodes = d3.hierarchy(data2, d => d.children)
  const lnkMkr = d3.linkHorizontal()
    .x(d => d.x)
    .y(d => d.y)

  // everything related to state initilization is placed 
  // under first render condition
  if(!plot.selectChildren().size()) {

    // ...
  }

  // ...
}

Теперь такие параметры, как масштабирование и прокрутка, не будут сбрасываться, так как они были инициализированы только один раз, и удаление всего контента не происходит

Плюсы и минусы этого метода:

Плюсы:

  • Простота реализации
  • Нет необходимости хранить функцию обновления для экземпляра графика
  • Одну и ту же функцию можно легко использовать для разных контейнеров, передав контейнер в качестве параметра функции.

Минусы:

  • Таким образом можно легко сохранить только состояние графика и его элементов.
  • Логика обновления размещена внутри функции построения графика. В сложных случаях это может быть сложно поддерживать

Извлечь логику обновления в функцию возврата

Если какое-то более сложное состояние, например пользовательский объект данных, не связанный напрямую с элементами визуализации, необходимо сохранить между обновлениями, один из возможных способов реализации жизненного цикла графика — вернуть функцию обновления из исходного рендеринга. В этом случае состояние будет сохранено при закрытии функции и будет доступно в функции обновления.

Главное учитывать, что функция обновления должна быть сохранена и доступна в приложении.

В приведенном ниже примере щелчок по узлам узлов сохранит свои данные во внутреннем объекте Set и будет использоваться между повторными рендерингами для выделения выбранных узлов другим цветом

// variable that will store render function
let updatePlot = null

function drawExample3() {
  const selected = new Set();

  // some general plot initialisation logic here
  // ...

  // initial call of render function
  render()

  // return of the render function
  return render

  // function that updates everything that depends on the data
  function render() {

  // ...
  }
}

// plot initialization call
updatePlot = drawExample3()

//...
// re-render function call
updatePlot()

Плюсы и минусы этого метода:

Плюсы:

  • Любой тип состояния может быть сохранен между обновлениями
  • Нет необходимости в дополнительной логике для обработки различных частей начального и обновленного рендеринга.

Минусы:

  • Функция обновления должна быть сохранена для экземпляра графика
  • Отдельная функция обновления может усложнить структуру сюжета.

Реализовать график как объект с функциями рендеринга и обновления

Для сложных случаев полезно структурировать логику сюжета как класс JavaScript. Разделение частей логики рендеринга/обновления на методы объекта может помочь обрабатывать сложные изменения данных и обновления визуальных компонентов. При правильной реализации легко использовать методы объекта отдельно или как часть общей логики обновления.

Такой способ структурирования сюжета дает еще больше преимуществ в управлении внутренним состоянием сюжета. Объекты JavaScript могут иметь свои собственные свойства, легко доступные через это ключевое слово внутри методов объекта. Это позволяет получить и установить состояние в каждом методе объекта графика или создать целый «API» внутри объекта для управления внутренним состоянием с правильными реакциями и обновлениями.

Как создать график как объект JavaScript — сложная тема, для объяснения которой требуется целая статья. Приведенный ниже код — это всего лишь демонстрация возможной реализации.

class Plot {
  constructor(state, data, ...otherProps) {
    // common object for internal state
    this.state = {
      ...state,
    }
    // applying initial data
    this.data = data

    // ...other initial props applying...

    this.render()
  }

  render = () => {
    // ...all the initial rendering logic...
  }

  setData = (data) => {
    this.data = data
    this.update()
  }

  setState = (newState) => {
    this.state = {
      ...newState
    }
    this.update()
  }

  update = () => {
    // ... all the update logic...
  }
}

const plot = new Plot(/*pass initial props here*/)

plot.setData(newData)

Плюсы и минусы этого метода:

Плюсы:

  • Для сложного состояния (например, с побочными эффектами обновления) легко создать внутренний «API» для обработки изменений.
  • Объект экземпляра графика предоставляет доступ к своим методам и состоянию, что упрощает работу с ним из приложения.
  • Все остальные преимущества использования классов и объектов

Минусы:

  • Необходимо знание работы с объектами в JavaScript.
  • Внутренняя сложность сюжетной логики может значительно возрасти

Заключение

Проблема состояния в диаграммах D3 может быть трудной для решения. Однако из-за невероятной гибкости, которую предоставляет библиотека, это всегда возможно. Точный способ реализации должен зависеть от начальной сложности проблемы и баланса между расширяемостью и простотой обслуживания.

Интерактивные примеры диаграмм и дополнительную информацию о D3 см. в моем блоге о визуализации данных:https://chartexample.com