Статьи

Научитесь создавать визуализации данных D3.js на примере

Эта статья была рецензирована Микаэлой Лер и Тимом Севериеном . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

D3.js — это библиотека JavaScript для управления документами на основе данных. D3 поможет вам оживить данные с помощью HTML, SVG и CSS.

Есть только три библиотеки JavaScript, которые я бы посоветовал каждому веб-разработчику: jQuery, Underscore и D3. Это библиотеки, которые позволяют вам по-новому взглянуть на код: jQuery позволяет писать меньше и делать больше с DOM, Underscore (или lodash) предоставляет вам функциональные инструменты для изменения способа написания программ, а D3 дает вам богатый инструмент. -установка для манипулирования данными и графического программирования. Если вы не знакомы с D3, пожалуйста, найдите время, чтобы взглянуть на впечатляющую галерею примеров, чтобы узнать, что можно с этим сделать.

Смотреть визуализировать данные с D3.js
Проиллюстрируйте свои данные с помощью JavaScript

Это не чертёжная библиотека твоего папы .

В 1786 году Уильям Плейфэйр изобрел гистограммы , линейные и районные диаграммы, а в 1801 году — круговую диаграмму . Сегодня это все еще основной способ представления большинства наборов данных. Теперь эти диаграммы превосходны, но D3 предоставляет вам инструменты и гибкость для создания уникальных визуализаций данных для Интернета, а ваш творческий потенциал является единственным ограничивающим фактором.

D3 — это чрезвычайно гибкая низкоуровневая библиотека визуализации с jQuery-подобным API для отображения данных в документах HTML и SVG. Он содержит множество полезных математических функций для преобразования данных и физических вычислений, хотя большая часть его мощности заключается в манипулировании геометриями и путями в SVG.

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

Гистограмма

Основная гистограмма

Смотрите кодекс

Я обещал вам больше, чем графики William Playfair, но создание скромной гистограммы с HTML — один из самых простых способов понять, как D3 преобразует данные в документ. Вот как это выглядит:

d3.select('#chart') .selectAll("div") .data([4, 8, 15, 16, 23, 42]) .enter() .append("div") .style("height", (d)=> d + "px") 

Функция selectAll возвращает D3 «выделение»: массив элементов, которые создаются, когда мы enter и append div для каждой точки данных.

Этот код отображает входные данные [4, 8, 15, 16, 23, 42] в этот выходной HTML.

 <div id="chart"> <div style="height: 4px;"></div> <div style="height: 8px;"></div> <div style="height: 15px;"></div> <div style="height: 16px;"></div> <div style="height: 23px;"></div> <div style="height: 42px;"></div> </div> 

Все свойства стиля, которые не меняются, можно использовать в CSS.

 #chart div { display: inline-block; background: #4285F4; width: 20px; margin-right: 3px; } 

Вкладная таблица GitHub

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

Таблица вклада в стиле GitHub

Смотрите кодекс

Вместо того, чтобы устанавливать высоту на основе значения данных, мы можем вместо этого установить background-color .

 const colorMap = d3.interpolateRgb( d3.rgb('#d6e685'), d3.rgb('#1e6823') ) d3.select('#chart') .selectAll("div") .data([.2, .4, 0, 0, .13, .92]) .enter() .append("div") .style("background-color", (d)=> { return d == 0 ? '#eee' : colorMap(d) }) 

Функция colorMap принимает входное значение от 0 до 1 и возвращает цвет по градиенту цветов между двумя предоставляемыми нами. Интерполяция является ключевым инструментом в графическом программировании и анимации, другие примеры мы увидим позже.

SVG Primer

Преимущество D3 заключается в том, что он работает с SVG, который содержит теги для рисования 2D-графики, такие как круги, многоугольники, контуры и текст.

Коллекция форм и текста

 <svg width="200" height="200"> <circle fill="#3E5693" cx="50" cy="120" r="20" /> <text x="100" y="100">Hello SVG!</text> <path d="M100,10L150,70L50,70Z" fill="#BEDBC3" stroke="#539E91" stroke-width="3"> </svg> 

Код выше рисует:

  • Круг на 50 120 с радиусом 20
  • Текст «Привет SVG!» На 100,100
  • Треугольник с 3px границей, атрибут d имеет следующие инструкции
    • Перейти к 100,10
    • Линия до 150,70
    • Линия до 50,70
    • Закрыть путь (Z)

<path> является самым мощным элементом в SVG.

круги

Маркированные круги, показывающие продажи по времени суток

Смотрите кодекс

Наборы данных в предыдущих примерах представляли собой простой массив чисел, D3 может работать и с более сложными типами.

 const data = [{ label: "7am", sales: 20 },{ label: "8am", sales: 12 }, { label: "9am", sales: 8 }, { label: "10am", sales: 27 }] 

Для каждой точки данных мы #chart элемент <g> (group) к #chart и #chart <circle> и <text> к каждому со свойствами из наших объектов.

 const g = d3.select('#chart') .selectAll("g") .data(data) .enter() .append('g') g.append("circle") .attr('cy', 40) .attr('cx', (d, i)=> (i+1) * 50) .attr('r', (d)=> d.sales) g.append("text") .attr('y', 90) .attr('x', (d, i)=> (i+1) * 50) .text((d)=> d.label) 

Переменная g содержит «выбор» d3, содержащий массив узлов <g> , такие операции, как append() добавляют новый элемент к каждому элементу в выборе.

Этот код отображает входные данные в этот документ SVG, вы можете увидеть, как это работает?

 <svg height="100" width="250" id="chart"> <g> <circle cy="40" cx="50" r="20"/> <text y="90" x="50">7am</text> </g> <g> <circle cy="40" cx="100" r="12"/> <text y="90" x="100">8am</text> </g> <g> <circle cy="40" cx="150" r="8"/> <text y="90" x="150">9am</text> </g> <g> <circle cy="40" cx="200" r="27"/> <text y="90" x="200">10am</text> </g> </svg> 

Линия Диаграмма

Основная линейная диаграмма

Смотрите кодекс

Рисовать линейную диаграмму в SVG довольно просто, мы хотим преобразовать данные следующим образом:

 const data = [ { x: 0, y: 30 }, { x: 50, y: 20 }, { x: 100, y: 40 }, { x: 150, y: 80 }, { x: 200, y: 95 } ] 

В этот документ:

 <svg id="chart" height="100" width="200"> <path stroke-width="2" d="M0,70L50,80L100,60L150,20L200,5"> </svg> 

Примечание : значения y вычитаются из высоты диаграммы (100), потому что мы хотим, чтобы значение y равное 100, было в верхней части svg (0 сверху).

Учитывая, что это только один элемент пути, мы могли бы сделать это сами с помощью кода, подобного следующему:

 const path = "M" + data.map((d)=> { return dx + ',' + (100 - dy); }).join('L'); const line = `<path stroke-width="2" d="${ path }"/>`; document.querySelector('#chart').innerHTML = line; 

D3 имеет функции генерации пути, чтобы сделать это намного проще, вот как это выглядит.

 const line = d3.svg.line() .x((d)=> dx) .y((d)=> 100 - dy) .interpolate("linear") d3.select('#chart') .append("path") .attr('stroke-width', 2) .attr('d', line(data)) 

Намного лучше! Функция interpolate имеет несколько различных способов рисования линии вокруг координат x, y. Посмотрите, как это выглядит с «линейным», «пошаговым», «базовым» и «кардинальным».

Линейный график в линейном стиле
Линейный график за шагом до стиля
Линейный график в базовом стиле
Линейный график в кардинальном стиле

Весы

Шкалы — это функции, которые отображают входной домен в выходной диапазон.

Смотрите кодекс

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

Представьте, что мы хотим визуализировать линейный график шириной 500 пикселей и высотой 200 пикселей со следующими данными:

 const data = [ { x: 0, y: 30 }, { x: 25, y: 15 }, { x: 50, y: 20 } ] 

В идеале мы хотим, чтобы значения оси y были от 0 до 30 (максимальное значение y), а значения оси x — от 0 до 50 (максимальное значение x), чтобы данные занимали все измерения диаграммы.

Мы можем использовать d3.max чтобы найти максимальные значения в нашем наборе данных и создать шкалы для преобразования наших входных значений x, y в выходные координаты x, y для наших путей SVG.

 const width = 500; const height = 200; const xMax = d3.max(data, (d)=> dx) const yMax = d3.max(data, (d)=> dy) const xScale = d3.scale.linear() .domain([0, xMax]) // input domain .range([0, width]) // output range const yScale = d3.scale.linear() .domain([0, yMax]) // input domain .range([height, 0]) // output range 

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

 xScale(0) -> 0 xScale(10) -> 100 xScale(50) -> 500 

Они также работают со значениями за пределами входного домена:

 xScale(-10) -> -100 xScale(60) -> 600 

Мы можем использовать эти шкалы в нашей функции генерации строк следующим образом:

 const line = d3.svg.line() .x((d)=> xScale(dx)) .y((d)=> yScale(dy)) .interpolate("linear") 

Еще одна вещь, которую вы можете легко сделать с помощью шкал, — указать отступы вокруг выходного диапазона:

 const padding = 20; const xScale = d3.scale.linear() .domain([0, xMax]) .range([padding, width - padding]) const yScale = d3.scale.linear() .domain([0, yMax]) .range([height - padding, padding]) 

Теперь мы можем визуализировать динамический набор данных, и наш линейный график всегда будет вписываться в границы 500px / 200px с отступом 20px со всех сторон.

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

Например, мы можем создать шкалу, которая отображает мою продолжительность жизни на число от 0 до 500:

 const life = d3.time.scale() .domain([new Date(1986, 1, 18), new Date()]) .range([0, 500]) // At which point between 0 and 500 was my 18th birthday? life(new Date(2004, 1, 18)) 

Анимированная визуализация полетов

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

Документ SVG для этого типа графики состоит из текста, линий и кругов.

 <svg id="chart" width="600" height="500"> <text class="time" x="300" y="50" text-anchor="middle">6:00</text> <text class="origin-text" x="90" y="75" text-anchor="end">MEL</text> <text class="dest-text" x="510" y="75" text-anchor="start">SYD</text> <circle class="origin-dot" r="5" cx="100" cy="75" /> <circle class="dest-dot" r="5" cx="500" cy="75" /> <line class="origin-dest-line" x1="110" y1="75" x2="490" y2="75" /> <!-- for each flight in the current time --> <g class="flight"> <text class="flight-id" x="160" y="100">JQ 500</text> <line class="flight-line" x1="100" y1="100" x2="150" y2="100" /> <circle class="flight-dot" cx="150" cy="100" r="5" /> </g> </svg> 

Динамические части — это время, элементы в летной группе, и данные могут выглядеть примерно так:

 let data = [ { departs: '06:00 am', arrives: '07:25 am', id: 'Jetstar 500' }, { departs: '06:00 am', arrives: '07:25 am', id: 'Qantas 400' }, { departs: '06:00 am', arrives: '07:25 am', id: 'Virgin 803' } ] 

Чтобы получить положение x для динамического времени, нам нужно создать шкалу времени для каждого рейса, которая отображает время его вылета и прибытия в положение x на нашем графике. Мы можем циклически перебирать наши данные, добавляя объекты и масштабы Date чтобы с ними было легче работать. Moment.js очень помогает здесь в разборе дат и манипуляциях.

 data.forEach((d)=> { d.departureDate = moment(d.departs, "hh-mm a").toDate(); d.arrivalDate = moment(d.arrives, "hh-mm a").toDate(); d.xScale = d3.time.scale() .domain([departureDate, arrivalDate]) .range([100, 500]) }); 

Теперь мы можем передать нашу изменяющуюся дату в xScale() чтобы получить координату x для каждого полета.

Render Loop

Время отправления и прибытия округляется до 5 минут, поэтому мы можем просматривать наши данные с шагом 5 м от первого отъезда до последнего прибытия.

 let now = moment(data[0].departs, "hh:mm a"); const end = moment(data[data.length - 1].arrives, "hh:mm a"); const loop = function() { const time = now.toDate(); // Filter data set to active flights in the current time const currentData = data.filter((d)=> { return d.departureDate <= time && time <= d.arrivalDate }); render(currentData, time); if (now <= end) { // Increment 5m and call loop again in 500ms now = now.add(5, 'minutes'); setTimeout(loop, 500); } } 

Вход, обновление и выход

D3 позволяет указывать преобразования и переходы элементов, когда:

  • Новые точки данных приходят (Enter)
  • Изменение существующих точек данных (обновление)
  • Существующие точки данных удалены (Выход)
 const render = function(data, time) { // render the time d3.select('.time') .text(moment(time).format("hh:mm a")) // Make a d3 selection and apply our data set const flight = d3.select('#chart') .selectAll('g.flight') .data(data, (d)=> d.id) // Enter new nodes for any data point with an id not in the DOM const newFlight = flight.enter() .append("g") .attr('class', 'flight') const xPoint = (d)=> d.xScale(time); const yPoint = (d, i)=> 100 + i * 25; newFlight.append("circle") .attr('class',"flight-dot") .attr('cx', xPoint) .attr('cy', yPoint) .attr('r', "5") // Update existing nodes in selection with id's that are in the data flight.select('.flight-dot') .attr('cx', xPoint) .attr('cy', yPoint) // Exit old nodes in selection with id's that are not in the data const oldFlight = flight.exit() .remove() } 

Переходы

Код выше отображает кадр каждые 500 мс с шагом 5 минут:

  • Обновляет время
  • Создает новую группу полета с кругом для каждого полета
  • Обновляет координаты х / у текущих рейсов
  • Удаляет группы полета, когда они прибыли

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

Например, давайте исчезнем в непрозрачности ввода групп полета.

 const newFlight = flight.enter() .append("g") .attr('class', 'flight') .attr('opacity', 0) newFlight.transition() .duration(500) .attr('opacity', 1) 

Давайте исчезнем с выходом из летных групп.

 flight.exit() .transition() .duration(500) .attr('opacity', 0) .remove() 

Добавьте плавный переход между точками x и y.

 flight.select('.flight-dot') .transition() .duration(500) .ease('linear') .attr('cx', xPoint) .attr('cy', yPoint) 

Мы также можем перевести время между 5-минутными приращениями, чтобы оно отображалось каждую минуту, а не каждые пять минут, используя функцию tween .

 const inFiveMinutes = moment(time).add(5, 'minutes').toDate(); const i = d3.interpolate(time, inFiveMinutes); d3.select('.time') .transition() .duration(500) .ease('linear') .tween("text", ()=> { return function(t) { this.textContent = moment(i(t)).format("hh:mm a"); }; }); 

t — значение прогресса между 0 и 1 для перехода.

Будь креативным

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

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

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