Статьи

Интерактивная визуализация данных с использованием современных JavaScript и D3

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

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

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

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

Есть три основных фактора, которые действительно выделяют D3 среди других библиотек:

  1. Гибкость D3 позволяет вам брать любые данные и напрямую связывать их с фигурами в окне браузера. Эти данные могут быть абсолютно любыми , что позволяет использовать огромное количество интересных вариантов использования для создания совершенно оригинальных визуализаций.
  2. Элегантность Легко добавлять интерактивные элементы с плавными переходами между обновлениями. Библиотека написана красиво , и как только вы освоите синтаксис, вы легко сможете сохранить ваш код в чистоте и порядке.
  3. Сообщество Уже есть обширная экосистема фантастических разработчиков, использующих D3, которые с готовностью делятся своим кодом в Интернете. Вы можете использовать такие сайты, как bl.ocks.org и blockbuilder.org, чтобы быстро находить заранее написанный код другими пользователями и копировать эти фрагменты непосредственно в свои собственные проекты.

Проэкт

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

Я начал изучать неравенство в доходах с помощью Google Public Data Explorer

Line chart showing how incomes have been changing over time in the US

При корректировке на инфляцию доходы домохозяйств оставались почти неизменными для 40% населения, хотя производительность на одного работника стремительно росла. Это действительно только первые 20% , которые получили больше преимуществ (и в этом диапазоне разница еще более шокирующая, если вы посмотрите на верхние 5%).

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

Черчение

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

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

Моя первая идея визуализации этих данных выглядела примерно так:

Мой первый набросок, изображающий «пирог становится больше»

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

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

Как на самом деле выглядел эскиз ...

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

Мой первый набросок, изображающий «пирог становится больше»

Вот как это выглядело на практике:

Как выглядел новый эскиз

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

Как это будет выглядеть в виде гистограммы

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

Заимствование Код

Итак, теперь, когда я знаю, что я собираюсь построить, пришло время познакомиться с настоящим проектом и начать писать код .

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

Мы создаем что-то совершенно новое, но оно имеет много общего с обычной круговой диаграммой, поэтому я быстро взглянул на bl.ocks.org и решил воспользоваться этой классической реализацией Майка Бостока, одного из Создатели D3. Этот файл, вероятно, уже был скопирован тысячи раз, и тот, кто написал его, настоящий мастер с JavaScript, поэтому мы можем быть уверены, что мы уже начинаем с хорошего блока кода.

Этот файл написан на D3 V3, который теперь является двумя устаревшими версиями, так как версия 5 была наконец выпущена в прошлом месяце. Большим изменением в D3 V4 стало то, что библиотека переключилась на использование плоского пространства имен, поэтому функции масштабирования, такие как d3.scale.ordinal() , записываются как d3.scaleOrdinal() . В версии 5 самым большим изменением стало то, что функции загрузки данных теперь структурированы как Promises , что упрощает обработку нескольких наборов данных одновременно.

Чтобы избежать путаницы, я уже столкнулся с проблемой создания обновленной версии этого кода для V5, которую я сохранил на blockbuilder.org . Я также преобразовал синтаксис в соответствии с соглашениями ES6, такими как переключение анонимных функций ES5 на функции стрелок.

Вот с чего мы уже начали:

Круговая диаграмма, с которой мы начинаем

Затем я скопировал эти файлы в свой рабочий каталог и убедился, что могу реплицировать все на своем компьютере. Если вы хотите самостоятельно следовать этому руководству, то можете клонировать этот проект из нашего репозитория GitHub . Вы можете начать с кода в файле starter.html . Обратите внимание, что для запуска этого кода вам понадобится сервер (такой как этот ), так как для получения данных он использует API Fetch .

Позвольте мне дать вам краткое изложение того, как работает этот код.

Ходить по нашему коду

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

 const width = 540; const height = 540; const radius = Math.min(width, height) / 2; 

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

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

 const svg = d3.select("#chart-area") .append("svg") .attr("width", width) .attr("height", height) .append("g") .attr("transform", `translate(${width / 2}, ${height / 2})`); 

Мы d3.select() пустой div с идентификатором области chart-area с помощью вызова d3.select() . Мы также присоединяем холст SVG с помощью d3.append() , и мы устанавливаем некоторые размеры для его ширины и высоты с помощью d3.attr() .

Мы также присоединяем элемент SVG group к этому холсту, который представляет собой особый тип элемента, который мы можем использовать для структурирования элементов вместе. Это позволяет нам сместить всю нашу визуализацию в центр экрана, используя атрибут transform элемента group.

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

 const color = d3.scaleOrdinal(["#66c2a5", "#fc8d62", "#8da0cb","#e78ac3", "#a6d854", "#ffd92f"]); 

Далее у нас есть несколько строк, которые настраивают круговую разметку D3:

 const pie = d3.pie() .value(d => d.count) .sort(null); 

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

Затем нам нужно определить генератор пути, который мы можем использовать для рисования наших дуг. Генераторы пути позволяют нам рисовать пути SVG в веб-браузере. Все, что на самом деле делает D3, — это связывает фрагменты данных с фигурами на экране, но в этом случае мы хотим определить более сложную фигуру, чем просто круг или квадрат. Path SVG работают путем определения маршрута для линии, между которой мы можем провести, которую мы можем определить с помощью ее атрибута d .

Вот как это может выглядеть:

 <svg width="190" height="160"> <path d="M10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80" stroke="black" fill="transparent"/> </svg> 

Вывод этого пути SVG-кода

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

Для дуги нам нужно дать нашему генератору пути значение innerRadius и outerRadius в пикселях, и генератор будет сортировать сложные математические вычисления, которые используются для вычисления каждого из углов для нас:

 const arc = d3.arc() .innerRadius(0) .outerRadius(radius); 

Для нашего графика мы используем нулевое значение для нашего innerRadius , что дает нам стандартную innerRadius диаграмму. Однако, если бы мы вместо этого хотели нарисовать кольцевую диаграмму , то все, что нам нужно было бы сделать, это вставить значение, которое меньше, чем наше значение outerRadius .

После нескольких объявлений функций мы загружаем наши данные с помощью функции d3.json() :

 d3.json("data.json", type).then(data => { // Do something with our data }); 

В D3 версии 5.x вызов d3.json() возвращает Promise , что означает, что D3 будет извлекать содержимое файла JSON, который он находит по относительному пути, который мы ему даем, и выполнять функцию, которую мы вызов метода then() после его загрузки. Затем у нас есть доступ к объекту, на который мы смотрим в аргументе data нашего обратного вызова.

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

 function type(d) { d.apples = Number(d.apples); d.oranges = Number(d.oranges); return d; } 

Если мы добавим console.log(data); В верхней части нашего обратного вызова d3.json мы можем взглянуть на данные, с которыми мы сейчас работаем:

 {apples: Array(5), oranges: Array(5)} apples: Array(5) 0: {region: "North", count: "53245"} 1: {region: "South", count: "28479"} 2: {region: "East", count: "19697"} 3: {region: "West", count: "24037"} 4: {region: "Central", count: "40245"} oranges: Array(5) 0: {region: "North", count: "200"} 1: {region: "South", count: "200"} 2: {region: "East", count: "200"} 3: {region: "West", count: "200"} 4: {region: "Central", count: "200"} 

Наши данные разделены на два разных массива, которые представляют наши данные для яблок и апельсинов соответственно.

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

 d3.selectAll("input") .on("change", update); 

Нам также нужно будет вызвать функцию update() при первом запуске нашей визуализации, передав начальное значение (с нашим массивом «яблок»).

 update("apples"); 

Давайте посмотрим, что делает наша функция update() . Если вы новичок в D3, это может вызвать некоторую путаницу, так как это одна из самых сложных частей D3 для понимания …

 function update(value = this.value) { // Join new data const path = svg.selectAll("path") .data(pie(data[value])); // Update existing arcs path.transition().duration(200).attrTween("d", arcTween); // Enter new arcs path.enter().append("path") .attr("fill", (d, i) => color(i)) .attr("d", arc) .attr("stroke", "white") .attr("stroke-width", "6px") .each(function(d) { this._current = d; }); } 

Во-первых, мы используем параметр функции по умолчанию для value . Если мы передадим аргумент нашей функции update() (когда мы запускаем ее впервые), мы будем использовать эту строку, иначе мы получим требуемое значение из события click наши радиовходы.

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

Прежде всего, есть наше соединение данных:

 // JOIN const path = svg.selectAll("path") .data(pie(data[val])); 

Каждый раз, когда обновляется наша визуализация, это связывает новый массив данных с нашими SVG на экране. Мы передаем наши данные (либо массив для «яблок», либо «апельсинов») в нашу функцию компоновки pie() , которая вычисляет некоторые начальные и конечные углы, которые можно использовать для рисования наших дуг. Эта переменная path теперь содержит специальный виртуальный выбор всех дуг на экране.

Затем мы обновляем все SVG на экране, которые все еще существуют в нашем массиве данных. Мы добавляем переход — фантастическая особенность библиотеки D3 — чтобы распространять эти обновления в течение 200 миллисекунд:

 // UPDATE path.transition().duration(200) .attrTween("d", arcTween); 

Мы используем метод d3.transition() вызове d3.transition() для определения пользовательского перехода, который D3 должен использовать для обновления позиций каждой из своих дуг (переход с атрибутом d ). Нам не нужно делать это, если мы пытаемся добавить переход к большинству наших атрибутов, но мы должны сделать это для перехода между различными путями. D3 не может понять, как переходить между пользовательскими путями, поэтому мы используем arcTween() чтобы дать D3 знать, как каждый из наших путей должен быть нарисован в каждый момент времени.

Вот как выглядит эта функция:

 function arcTween(a) { const i = d3.interpolate(this._current, a); this._current = i(1); return t => arc(i(t)); } 

Мы используем d3.interpolate() здесь, чтобы создать то, что называется интерполятором . Когда мы вызываем функцию, которую мы храним в переменной i со значением от 0 до 1, мы возвращаем значение, которое находится где-то между this._current и a . В этом случае this._current — это объект, который содержит начальный и конечный угол this._current фрагмента, на который мы смотрим, и a представляет новый a до которого мы обновляем.

Как только мы настроили интерполятор, мы обновляем значение this._current чтобы оно содержало значение, которое у нас будет в конце ( i(a) ), а затем мы возвращаем функцию, которая вычислит путь, который наша дуга должна содержать, основываясь на этом значении t . Наш переход будет запускать эту функцию на каждом такте его часов (передавая аргумент между 0 и 1), и этот код будет означать, что наш переход будет знать, где должны быть нарисованы наши дуги в любой момент времени.

Наконец, наша функция update() должна добавить новые элементы, которых не было в предыдущем массиве данных:

 // ENTER path.enter().append("path") .attr("fill", (d, i) => color(i)) .attr("d", arc) .attr("stroke", "white") .attr("stroke-width", "6px") .each(function(d) { this._current = d; }); 

Этот блок кода установит начальные позиции каждой из наших дуг при первом запуске этой функции обновления. Здесь метод enter() дает нам все элементы в наших данных, которые необходимо добавить на экран, и затем мы можем циклически перебирать каждый из этих элементов с помощью методов attr() , чтобы установить заполнение и положение каждого из наших дуги. Мы также даем каждой из наших дуг белую рамку, что делает наш график более аккуратным. Наконец, мы устанавливаем свойство this._current каждой из этих дуг в качестве начального значения элемента в наших данных, которое мы используем в функции arcTween() .

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

Это подводит нас к следующему шагу в процессе …

Адаптирующий код

Теперь, когда у нас есть некоторый код в нашей локальной среде, и мы понимаем, что он делает, я собираюсь переключить данные, на которые мы смотрим, так, чтобы они работали с данными, которые нас интересуют.

Я включил данные, с которыми мы будем работать, в папку data/ нашего проекта. Поскольку на этот раз этот новый incomes.csv находится в формате CSV (этот тип файлов можно открыть в Microsoft Excel), я буду использовать d3.csv() вместо d3.json() функция:

 d3.csv("data/incomes.csv").then(data => { ... }); 

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

Если вы добавите оператор console.log(data) в d3.csv обратного вызова d3.csv , вы сможете увидеть форму данных, с которыми мы работаем:

 (50) [{}, {}, {}, {}, {}, {}, {} ... columns: Array(9)] 0: 1: "12457" 2: "32631" 3: "56832" 4: "92031" 5: "202366" average: "79263" top: "350870" total: "396317" year: "2015" 1: {1: "11690", 2: "31123", 3: "54104", 4: "87935", 5: "194277", year: "2014", top: "332729", average: "75826", total: "379129"} 2: {1: "11797", 2: "31353", 3: "54683", 4: "87989", 5: "196742", year: "2013", top: "340329", average: "76513", total: "382564"} ... 

У нас есть массив из 50 элементов, каждый из которых представляет год в наших данных. Для каждого года у нас есть объект с данными для каждой из пяти групп доходов, а также несколько других полей. Мы могли бы создать здесь круговую диаграмму за один из этих лет, но сначала нам нужно немного перемешать наши данные, чтобы они были в правильном формате. Когда мы хотим написать соединение данных с D3, нам нужно передать массив, где каждый элемент будет привязан к SVG.

Напомним, что в нашем последнем примере у нас был массив с элементом для каждого кусочка пирога, который мы хотели отобразить на экране. Сравните это с тем, что у нас есть на данный момент, это объект с клавишами от 1 до 5, представляющими каждый круговой фрагмент, который мы хотим нарисовать.

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

 function prepareData(d){ return { name: d.year, average: parseInt(d.average), values: [ { name: "first", value: parseInt(d["1"]) }, { name: "second", value: parseInt(d["2"]) }, { name: "third", value: parseInt(d["3"]) }, { name: "fourth", value: parseInt(d["4"]) }, { name: "fifth", value: parseInt(d["5"]) } ] } } d3.csv("data/incomes.csv", prepareData).then(data => { ... }); 

Каждый год эта функция будет возвращать объект с массивом values , который мы передадим в наше объединение данных. Мы помечаем каждое из этих значений в поле name и присваиваем им числовое значение, основанное на значениях дохода, которые у нас уже были. Мы также отслеживаем средний доход каждого года для сравнения.

На данный момент у нас есть данные в формате, с которым мы можем работать:

 (50) [{}, {}, {}, {}, {}, {}, {} ... columns: Array(9)] 0: average: 79263 name: "2015" values: Array(5) 0: {name: "first", value: 12457} 1: {name: "second", value: 32631} 2: {name: "third", value: 56832} 3: {name: "fourth", value: 92031} 4: {name: "fifth", value: 202366} 1: {name: "2014", average: 75826, values: Array(5)} 2: {name: "2013", average: 76513, values: Array(5)} ... 

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

На данный момент наши данные начинаются в 2015 году и заканчиваются в 1967 году, поэтому нам нужно обратить этот массив, прежде чем делать что-либо еще:

 d3.csv("data/incomes.csv", prepareData).then(data => { data = data.reverse(); ... }); 

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

 const pie = d3.pie() .value(1) .sort(null); 

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

 d3.csv("data/incomes.csv", prepareData).then(data => { data = data.reverse(); const radiusScale = d3.scaleSqrt() .domain([0, data[49].values[4].value]) .range([0, Math.min(width, height) / 2]); ... }); 

Мы добавляем эту шкалу, как только у нас будет доступ к нашим данным, и мы говорим, что наш вклад должен находиться в диапазоне от 0 до наибольшего значения в нашем наборе данных, который является доходом от самой богатой группы за последний год в наших данных ( data[49].values[4].value ). Для домена мы устанавливаем интервал, между которым должно выходить наше выходное значение.

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

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

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

 function update(value = this.value) { arc.outerRadius(d => radiusScale(d.data.value)); ... }); 

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

Мы также должны удалить наш вызов outerRadius когда мы первоначально настроили наш генератор дуги, чтобы у нас было это в верхней части нашего файла:

 const arc = d3.arc() .innerRadius(0); 

Наконец, нам нужно внести несколько изменений в эту функцию update() , чтобы все совпадало с нашими новыми данными:

 function update(data) { arc.outerRadius(d => radiusScale(d.data.value)); // JOIN const path = svg.selectAll("path") .data(pie(data.values)); // UPDATE path.transition().duration(200).attrTween("d", arcTween); // ENTER path.enter().append("path") .attr("fill", (d, i) => color(i)) .attr("d", arc) .attr("stroke", "white") .attr("stroke-width", "2px") .each(function(d) { this._current = d; }); } 

Поскольку мы больше не собираемся использовать наши переключатели, я просто передаю объект year, который мы хотим использовать, вызывая:

 // Render the first year in our data update(data[0]); 

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

Наш график за первый год наших данных

Делать это динамичным

Следующим шагом будет цикл визуализации между разными годами, показывающий, как доходы менялись с течением времени. Мы сделаем это, добавив вызов функции JavaScript setInterval() , которую мы можем использовать для многократного выполнения некоторого кода:

 d3.csv("data/incomes.csv", prepareData).then(data => { ... function update(data) { ... } let time = 0; let interval = setInterval(step, 200); function step() { update(data[time]); time = (time == 49) ? 0 : time + 1; } update(data[0]); }); 

Мы устанавливаем таймер в этой переменной time , и каждые 200 мс этот код будет запускать функцию step() , которая обновит наш график до данных следующего года и увеличит таймер на 1. Если таймер на значение 49 (последний год по нашим данным) будет сброшено само собой. Теперь это дает нам хороший цикл, который будет работать непрерывно:

Наш график обновления между каждым годом в наших данных

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

 <h2>Year: <span id="year"></span></h2> <div class="container" id="page-main"> <div class="row"> <div class="col-md-7"> <div id="chart-area"></div> </div> <div class="col-md-5"> <table class="table"> <tbody> <tr> <th></th> <th>Income Bracket</th> <th>Household Income (2015 dollars)</th> </tr> <tr> <td id="leg5"></td> <td>Highest 20%</td> <td class="money-cell"><span id="fig5"></span></td> </tr> <tr> <td id="leg4"></td> <td>Second-Highest 20%</td> <td class="money-cell"><span id="fig4"></span></td> </tr> <tr> <td id="leg3"></td> <td>Middle 20%</td> <td class="money-cell"><span id="fig3"></span></td> </tr> <tr> <td id="leg2"></td> <td>Second-Lowest 20%</td> <td class="money-cell"><span id="fig2"></span></td> </tr> <tr> <td id="leg1"></td> <td>Lowest 20%</td> <td class="money-cell"><span id="fig1"></span></td> </tr> </tbody> <tfoot> <tr> <td id="avLeg"></td> <th>Average</th> <th class="money-cell"><span id="avFig"></span></th> </tr> </tfoot> </table> </div> </div> </div> 

Мы структурируем нашу страницу здесь, используя сеточную систему Bootstrap , которая позволяет нам аккуратно форматировать наши элементы страницы в блоки.

Затем я буду обновлять все это с помощью jQuery при каждом изменении наших данных:

 function updateHTML(data) { // Update title $("#year").text(data.name); // Update table values $("#fig1").html(data.values[0].value.toLocaleString()); $("#fig2").html(data.values[1].value.toLocaleString()); $("#fig3").html(data.values[2].value.toLocaleString()); $("#fig4").html(data.values[3].value.toLocaleString()); $("#fig5").html(data.values[4].value.toLocaleString()); $("#avFig").html(data.average.toLocaleString()); } d3.csv("data/incomes.csv", prepareData).then(data => { ... function update(data) { updateHTML(data); ... } ... } 

Я также внесу несколько изменений в CSS вверху нашего файла, которые дадут нам легенду для каждой из наших дуг, а также центрируют наш заголовок:

 <style> #chart-area svg { margin:auto; display:inherit; } .money-cell { text-align: right; } h2 { text-align: center; } #leg1 { background-color: #66c2a5; } #leg2 { background-color: #fc8d62; } #leg3 { background-color: #8da0cb; } #leg4 { background-color: #e78ac3; } #leg5 { background-color: #a6d854; } #avLeg { background-color: grey; } @media screen and (min-width: 768px) { table { margin-top: 100px; } } </style> 

То, что мы в итоге получаем, довольно презентабельно:

Наш график после добавления в таблицу и некоторые стили

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

 d3.csv("data/incomes.csv", prepareData).then(data => { ... update(data[0]); data[0].values.forEach((d, i) => { svg.append("circle") .attr("fill", "none") .attr("cx", 0) .attr("cy", 0) .attr("r", radiusScale(d.value)) .attr("stroke", color(i)) .attr("stroke-dasharray", "4,4"); }); }); 

Для этого я использую метод Array.forEach() , хотя я мог бы также снова использовать обычный шаблон общего обновления D3 (JOIN / EXIT / UPDATE / ENTER).

Я также хочу добавить в строку, чтобы показать средний доход в США, который я буду обновлять каждый год. Сначала я добавлю среднюю строку в первый раз:

 d3.csv("data/incomes.csv", prepareData).then(data => { ... data[0].values.forEach((d, i) => { svg.append("circle") .attr("fill", "none") .attr("cx", 0) .attr("cy", 0) .attr("r", radiusScale(d.value)) .attr("stroke", color(i)) .attr("stroke-dasharray", "4,4"); }); svg.append("circle") .attr("class", "averageLine") .attr("fill", "none") .attr("cx", 0) .attr("cy", 0) .attr("stroke", "grey") .attr("stroke-width", "2px"); }); 

Затем я обновлю это в конце нашей функции update() каждый раз, когда меняется год:

 function update(data) { ... svg.select(".averageLine").transition().duration(200) .attr("r", radiusScale(data.average)); } 

Я должен отметить, что для нас важно добавить каждый из этих кругов после нашего первого вызова update() , потому что в противном случае они в конечном итоге будут отрисовываться за каждым из наших путей дуг (слои SVG определяются порядком, в котором они ‘ добавлены на экран, а не по их z-индексу).

На данный момент у нас есть кое-что, что передает данные, с которыми мы работаем, немного более четко:

Наш график после добавления в некоторые линии сетки за первый год наших данных

Делаем это интерактивным

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

Вот HTML-код, который я буду использовать для добавления этих элементов на экран:

 <div class="container" id="page-main"> <div id="controls" class="row"> <div class="col-md-12"> <button id="play-button" class="btn btn-primary">Play</button> <div id="slider-div"> <label>Year: <span id="year-label"></span></label> <div id="date-slider"></div> </div> </div> </div> ... </div> 

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

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

 function update(data) { ... let time = 0; let interval; function step() { update(data[time]); time = (time == 49) ? 0 : time + 1; } $("#play-button").on("click", function() { const button = $(this); if (button.text() === "Play"){ button.text("Pause"); interval = setInterval(step, 200); } else { button.text("Play"); clearInterval(interval); } }); ... } 

Всякий раз, когда нажимается наша кнопка, наш блок if/else здесь будет определять другое поведение в зависимости от того, является ли наша кнопка кнопкой «Play» или кнопкой «Pause». Если кнопка, которую мы нажимаем, говорит «Play», мы заменим кнопку на кнопку «Pause» и запустим наш цикл интервала. В качестве альтернативы, если кнопка представляет собой кнопку «Пауза», мы изменим ее текст на «Воспроизведение» и будем использовать функцию clearInterval() чтобы остановить цикл.

Для нашего слайдера я хочу использовать слайдер, который поставляется с библиотекой пользовательского интерфейса jQuery . Я включил это в наш HTML, и я собираюсь написать несколько строк, чтобы добавить это на экран:

 function update(data) { ... $("#date-slider").slider({ max: 49, min: 0, step: 1, slide: (event, ui) => { time = ui.value; update(data[time]); } }); update(data[0]); ... } 

Здесь мы используем параметр slide чтобы прикрепить прослушиватель событий к слайдеру. Всякий раз, когда наш ползунок перемещается к другому значению, мы обновляем наш таймер на это новое значение, и мы запускаем нашу функцию update() в тот год в наших данных.

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

 function update(data) { ... // Update slider position $("#date-slider").slider("value", time); } 

Я также добавлю строку в нашу updateHTML() (которая запускается всякий раз, когда изменяется наша визуализация), которая может корректировать значение метки на основе текущего года в данных:

 function updateHTML(data) { // Update title $("#year").text(data.name); // Update slider label $("#year-label").text(data.name); // Update table values $("#fig1").html(data.values[0].value.toLocaleString()); ... } 

Я добавлю еще несколько строк в наш CSS, чтобы все выглядело немного лучше:

 <style> ... @media screen and (min-width: 768px) { table { margin-top: 100px; } } #page-main { margin-top: 10px; } #controls { margin-bottom: 20px; } #play-button { margin-top: 10px; width: 100px; } #slider-div { width:300px; float:right; } </style> 

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

Наш график после добавления в некоторые интерактивные элементы

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

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

  • Обзор содержимого SitePoint D3.js.
  • Введение в библиотеку на домашней странице D3. Это запускает некоторые из самых основных команд, показывая вам, как сделать первые несколько шагов в D3.
  • « Давайте создадим гистограмму » Майка Бостока, создателя D3, — показывающую новичкам, как сделать один из самых простых графиков в библиотеке.
  • D3.js в действии Элайджи Микс (35 долларов), это солидный вводный учебник, в котором много деталей.
  • Slack канал D3 очень радушен для новичков в D3. В нем также есть раздел «учебные материалы» с большим количеством ресурсов.
  • Это онлайн-курс Удеми (20 долларов), который охватывает все, что есть в библиотеке, в виде серии видео-лекций. Он предназначен для разработчиков JavaScript и включает в себя четыре интересных проекта.
  • Множество примеров визуализаций, которые доступны на bl.ocks.org и blockbuilder.org.
  • Справочник по D3 API , который дает подробное техническое объяснение всего, что может предложить D3.

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