Как прототипировать диаграммы D3 в React неправильным способом

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

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

D3 и React борются за DOM

Внедрение D3 в React для интеграции управляемых данными элементов в пользовательский интерфейс всегда начинается с одного и того же фундаментального вопроса:
Кто контролирует DOM?
React не обнаруживает изменений в DOM. Новые реквизиты или вызов setState инициируют вычисление внутреннего представления DOM, а обновления основаны на сравнении нового представления DOM с предыдущим деревом. реквизит.

Разделяй и властвуй

Следовательно, подход D3 к обновлению моделей и поддержанию их синхронизации с представлением не может сосуществовать с элементом управления DOM React.

Есть два решения этой дилеммы, которые позволяют использовать возможности D3 внутри приложения React:

1. Оставить DOM для React, используя D3 только для вычислений таких вещей, как масштабы или пути.
2. Блокирование React от обновления всех дочерних элементов ниже корневого узла D3.

Сразу скажу:
Для более сложных проектов первый подход предпочтительнее и широко считается правильным.
Важно знать, что D3 не является диаграмм, а набор инструментов низкого уровня, разработанный Майком Бостоком, когда он работал над созданием пользовательских визуализаций для New York Times.
Даже такие соглашения, как «соглашение о многоразовых диаграммах», имеют ограниченную возможность повторного использования и гибкость, потому что они не раскрывать отдельные элементы объединения данных.
Подход, ориентированный на React, позволяет создавать более многоразовые диаграммы, которые органично интегрируются в React.

Минусов у такого подхода всего несколько:

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

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

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

Прототипы, построенные таким образом, должны отвечать двум основным требованиям:

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

Как создать прототип внутри React с помощью соединений данных D3

Существуют паттерны, благодаря которым очень удобно создать рабочий прототип диаграммы, но при этом иметь возможность передать DOM в React на более позднем этапе без необходимости начинать с нуля.

Принятие прагматизма

Важно понимать, что тот тип общности, который требуется библиотеке, чтобы быть полезным для самых разных вариантов использования при сохранении стабильного API, часто ортогонален гибкости, позволяющей быстро изменять и тестировать различные свойства и варианты прототипа.

  • принятие ограниченного повторного использования многих частей кода D3, небольшое копирование и вставка имеют большое значение
  • отказ от дорогостоящих шаблонов, таких как многоразовые соглашения о диаграммах, которые по-прежнему не обеспечивают доступ к отдельным частям соединения данных
  • зная, когда нужно переключиться на подход, ориентированный на React
  • избегание кода пахнет спекулятивной общностью и никогда не забывайте: «Вам это не понадобится»

Идемпотентные соединения данных и контейнеры с одним элементом

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

Вместо двух отдельных функций с результатами, которые зависят от текущего состояния DOM.

function initChart(){...}
function updateChart(){...}

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

function chart (){...}

Создание контейнеров с использованием шаблона одного элемента очень просто:

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

Уменьшение связи между данными и представлением

Использование функций доступа вместо прямого обращения к отдельным свойствам точек данных уменьшает связь между формой данных и их визуальным представлением.
По-прежнему важно быть прагматичным, каждый ненужный уровень косвенности также имеет свои издержки. Обычно разумным подходом является начать с определения методов доступа x и y для создания путей SVG и добавления дополнительных слоев, когда это необходимо.

const x = datum => datum.date 
const line = d3.line().x(d => xScale(x(d)))  .y(d => yScale(y(d)))

Никогда не масштабируйте данные напрямую

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

Только чистые функции для преобразования данных

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

const getEvent = datum => datum.event
const is = x => y => x===y
const over = value => x => x > value
const getScore = datum => datum.score
const isBigScore = compose(over(10),getScore)
const isGame = compose(is('game'),getEvent)
const onlyBigGames data.filter(datum=>isBigScore(datum)&&isGame(datum))

Отдельная логика и нечистые соединения данных

Даже идемпотентные соединения данных D3 по-прежнему зависят от состояния DOM и являются нечистыми. Сохранение как можно большего количества логики для чистых преобразований данных и сохранение нечистого слоя соединения данных D3 как можно более тонким делает код более удобным для тестирования и повышает предсказуемость.
Слой данных можно повторно использовать при переходе на подход, контролируемый React, или введение библиотеки графиков.

React State как единственный источник правды

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

this.state = {activeEvent:'birthday', events: [{score: 15, type:'birthday'}, ...]}
instead of directly storing derived state like
this.state = {activeEvent:'birthday', events: [{score: 15, type:'birthday', isActiveEvent: true }, ...]}

Если nextState зависит от предыдущего состояния, this.setState требует передать ему функцию из-за его асинхронной природы.

const inc = x => x+1
const incSelectionCount = previousState => ({...previousState, selectionCount: inc(previousState.selectionCount)})
...
this.setState(inSelectionCount)

Это также связано с улучшенной тестируемостью.

Что тестировать?

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

Еще несколько полезных вещей

Рекомендуется сохранить отдельные части соединения данных и присвоить им соответствующие имена:

Части, которые создают только контейнеры, такие как соглашения о полях, больше всего выигрывают от создания небольших повторно используемых вспомогательных функций, подобных следующему примеру:

const calcInnerSize = props => {
const { margin, outerSize } = props
const width = outerSize.width - margin.left - margin.right
const height = outerSize.height - margin.top - margin.bottom
return ({ width, height })

Почти каждая диаграмма требует некоторого заполнения по оси y, что легко реализовать следующим образом:

Маленький грязный секрет отладки

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

Вывод

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

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

Куда смотреть дальше?

  • Я рекомендую взглянуть на удивительный react-spring для захватывающего подхода к объединению производительной анимации в React с декларативным API.
  • Шаблон украшения d3fc — это захватывающее расширение соглашения о многоразовых диаграммах, которое предоставляет пользователю различные части соединения данных.
  • vx — это растущая коллекция низкоуровневых компонентов визуализации, которые обеспечивают отличную основу для создания пользовательских многоразовых визуализаций, ориентированных на React.