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

Перед началом, если вы уже знакомы с D3.js или его основным пользователем, или если вы говорите: Обсуждение дешево. Покажи мне код . person, то вот ссылка, по которой вы можете найти окончательный код. Что касается остальных, давайте приступим!

Гонка на гистограмме

Просто представьте, что вы на уроке геометрии, а предметом является система координат. По сути, наша визуализация представляет собой гистограмму с координатами x и y. Начало координат находится в верхнем левом углу. Ось X масштабируется от верхнего левого угла к верхнему правому углу. А точки на оси x показывают величину полосок, в моем наборе данных они соответствуют value, подробности см. В разделе Набор данных. Ось Y, с другой стороны, , масштабируется от верхнего левого угла к нижнему левому углу. Значения / точки на оси Y являются непрерывными, но, что удивительно, эти точки не будут вычисляться с помощью числовых значений. Для расчета этих точек вы собираетесь использовать порядковые значения, в моем наборе данных они соответствуют name. Этой магией занимается D3.js, вы увидите это в следующем разделе.

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

Набор данных

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

Используемые данные будут представлять собой массив, массив каждого периода времени, в этом примере - лет. Каждый элемент в массиве dataSets имеет поля date и dataSet. Поле dataSet содержит точки данных, представляющие каждую полосу. У каждой панели есть поля имя и значение. name будет использоваться для вычисления координаты y каждого столбца, а value будет использоваться для вычисления координаты x.

Примечание. Я ограничил значения минимальным числом, чтобы названия полос были видны. Но в моем дизайне он особенный, в вашем он вам может и не понадобиться.

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

Стартовый проект

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

Элемент SVG

<svg id="bar-chart-race">
  <g class="chart-container">
    <text class="chart-title"></text>
    <g class="x-axis"></g>
    <g class="y-axis"></g>
    <g class="columns"></g>
    <text class="current-date"></text>
  </g>
</svg>

Первое, что вы должны понять, это элемент svg в index.html. Именно здесь все происходит. Наша визуализация, сама гонка гистограмм, определяется и реализуется с помощью элемента SVG. Мы обновим его программно, благодаря библиотеке D3.js. Если вам интересно, что такое SVG-элемент, загляните сюда.

Есть пара g элементов, этот элемент SVG обычно используется для группировки других элементов SVG и text элементов для отображения заголовка диаграммы и информационного текста года. Они просто используются, как и другие элементы HTML. Мы будем поставлять контент динамически, используя d3 функций, изначально они пустые.

Я установил статические значения ширины и высоты для элемента SVG в файле CSS, но вы также можете установить эти значения.

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

Оказывать

const myChart = new BarChartRace("bar-chart-race");
myChart
  .setTitle("Bar Chart Race Title")
  .addDatasets(generateDataSets({ size: 5 }))
  .render();

Если вы отметите index.js, вы увидите, что создан новый экземпляр диаграммы. У него есть несколько методов, и эти методы вызываются в специальном механизме, то есть в цепочке методов. Вы увидите, как это достигается позже. А теперь займемся исследованием BarChartRace!

BarChartRace

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

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

const chartContainer = d3.select(`#${chartId} .chart-container`);
const xAxisContainer = d3.select(`#${chartId} .x-axis`);
const yAxisContainer = d3.select(`#${chartId} .y-axis`);

D3 имеет два разных метода: d3.select и d3.selectAll. Эти методы возвращают выборку DOM. Они принимают аргумент в качестве селектора. Если вы знакомы с функциями document.querySelector и document.querySelectorAll, селекторы принимаются в формате строки селекторов W3C. Используя эти методы выбора, вы можете вносить изменения в элементы DOM. Для получения подробной информации посетите страницу github.

d3.select(`#${chartId}`)
  .attr("width", chartSettings.width)
  .attr("height", chartSettings.height);

Вот как вы можете использовать выделение для внесения изменений в выбранные элементы DOM. attr можно использовать метод добавления / изменения html-атрибута выбранного элемента. Он принимает два аргумента: первый - это имя атрибута, а второй - значение атрибута. Но вторые аргументы могут быть либо примитивным значением, либо функцией обратного вызова. Вы можете увидеть, как использовать функции обратного вызова в следующих разделах.

Помимо методов выбора, D3.js имеет множество полезных методов для создания визуальных элементов и подготовки данных для них. Давайте вспомним наш класс геометрии выше, у нас есть ось x и ось y. А для создания оси в D3.js. есть специальные методы. Но прежде чем создавать визуальные элементы для компонента оси, нам нужно создать для него масштаб. Scales - специальные функции, используемые для отображения абстрактных данных в визуальное представление. Что это значит? Давайте рассмотрим нашу ось X, чтобы лучше понять. В нашем дизайне элемент оси x имеет в целом ширину 420 пикселей (т. Е. Внутренняя ширина рассчитывается с учетом заполнения диаграммы). Это значение ширины является единицей измерения браузера. Однако значения в наших наборах данных не в пикселях. У них просто значения как числа. Как рассчитать соответствующую ширину каждой полосы в пикселях? Для этого мы воспользуемся методом scale. В методах масштабирования мы определим масштаб с помощью domain и range.

const xAxisScale = d3.scaleLinear().range([0, chartSettings.innerWidth]);

Для нашей оси x мы используем scaleLinear, так как нам нужен непрерывный масштаб. xAxisScale имеет диапазон от 0 до 420. Мы также определим domain для масштаба, это также будет диапазон из-за линейного масштаба, но мы установим домен в более позднем разделе, внутри функции draw.

Для yAxisScale вы увидите, что используется метод scaleBand. Для оси Y мы будем использовать строковые значения района, имя каждой полосы столбца, а с помощью scaleBand мы можем сопоставить эти значения с непрерывным диапазоном. Мы также добавили padding к шкале оси Y, чтобы отделить столбцы друг от друга и сделать промежутки между ними.

Вы можете посмотреть другие функции в BarChartRace, в стартовом проекте я не буду объяснять, за что еще отвечают другие функции. Думаю, вы поймете их причины. Но прежде чем перейти к следующему разделу, я хочу объяснить, как вызывать методы с помощью цепочки методов. Вы помните это, не так ли? На самом деле это так просто :) Вам нужно только вернуть this объект. Вот как вы можете вызывать функции по цепочке.

function addDatasets(dataSets){
  chartDataSets.push.apply(chartDataSets, dataSets);
  return this;
}

Мы посмотрели на коды наших стартовых проектов, теперь нам нужно реализовать два важных метода draw и render. Приступим к работе с draw!

С этого момента вы можете следить за кодами в финальном проекте. Я просто покажу важные блоки кода.

Функция рисования

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

xAxisScale.domain([0, dataSetDescendingOrder[0].value]);
yAxisScale.domain(dataSetDescendingOrder.map(({ name }) => name));
xAxisContainer.transition(transition).call(
  d3
    .axisTop(xAxisScale)
    .ticks(ticksInXAxis)
    .tickSize(-innerHeight)
  );
yAxisContainer
  .transition(transition)
  .call(d3.axisLeft(yAxisScale).tickSize(0));

Мы оставили функции масштабирования незавершенными для обеих осей, вы помните? Вот наши недостающие части для них. Мы определяем domain для обеих осей в зависимости от нашего текущего набора данных, то есть dataSetDescendingOrder. Для оси X имеется непрерывный диапазон, мы определяем домен, устанавливая минимальное и максимальное значения в наборе данных. При создании случайных данных я использовал 0 (ноль) для минимального значения, а первый элемент массива dataSet будет максимальным из-за порядка убывания. В зависимости от ваших данных вам необходимо рассчитать эти значения диапазона самостоятельно. Для оси Y, имеющей категориальные значения, мы определяем домен как массив names.

У нас есть области для масштабных функций. И что? В чем здесь смысл, спросите вы. Что ж, эти служебные функции помогают нам создать отображение. Мы будем использовать их в двух разных точках. Во-первых, чтобы создать элементы оси, то есть xAxisContainer и yAxisContainer. Они создадут и добавят необходимые элементы в DOM. Выше, как вы можете видеть, есть методы axisTop и axisLeft для создания связанных с осью элементов SVG. Подробности смотрите здесь. tickSize(-innerHeight) - это трюк для отображения линий сетки для значений деления оси x. Также проверьте правила css для обеих осей, потому что некоторые тексты и строки невидимы. Для используемых функций масштабирования второго места, чтобы найти значение сопоставления, проверьте это в следующем разделе.

Вы заметили функцию transition в приведенных выше строках кода. Может быть, вы спросили себя, а это что-то полезное? Что ж, эта специальная функция отвечает за создание анимации. Вау, теперь у нас есть возможность показывать что-то с помощью анимации. Но как эта функция создает для нас анимацию? Ответ кроется в его названии. Он будет переводить элементы из текущего состояния в желаемое. Зная текущее и желаемое состояния, d3 применяет изменения между двумя состояниями, интерполируя эти значения. Некоторые примитивные значения, такие как строки или числа, можно интерполировать с помощью встроенных интерполяторов, кроме того, вы можете создавать свои собственные интерполяторы. В следующих частях этого раздела есть пример пользовательских интерполяторов.

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

Общий паттерн обновления

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

const barGroups = chartContainer
  .select(".columns")
  .selectAll("g.column-container")
  .data(dataSetDescendingOrder, ({ name }) => name);

Выбираем все g элементы, имеющие имя класса column-container. Хммм, но здесь, похоже, есть проблема, поскольку у нас еще нет отрисовки этого элемента в DOM. Мы не добавляем их ни в файл html явно, ни в файл javascript динамически. Итак, это пока пустой выбор. Но мы используем другой метод D3.js, то есть data. Этот специальный метод связывает наши данные, в нашем случае массив, с выбранными элементами. Второй параметр функции data получает ключевую функцию. Он решает, как различать каждый элемент данных. Если вы знакомы с одной из популярных интерфейсных фреймворков, она похожа на оператор key. Я просто использовал поле name в своих данных, если у вас есть какое-либо уникальное поле в ваших данных, например id, вы можете его использовать.

Функция data возвращает выбор, который представляет выбор обновления. Теперь вы можете использовать выбор enter и exit.

Введите выбор

Вы успешно связали данные. Теперь вы можете добавлять элементы в DOM в зависимости от ваших данных.

const barGroupsEnter = barGroups
  .enter()
  .append("g")
  .attr("class", "column-container")
  .attr("transform", `translate(0,${innerHeight})`);

Когда вы вызываете функцию enter, вы фактически создаете элементы в DOM и устанавливаете начальные настройки для первого появления. Каждый раз, когда новые данные, т.е. имеющие уникальное значение ключа, добавляются с помощью функции data, будут выполняться настройки для элемента, функции идут после enter.

В нашем случае мы добавляем элементы столбца-контейнера в DOM с помощью функции append. Мы устанавливаем его начальную позицию как самую нижнюю линию графика, translate(0,${innerHeight}). И в следующих строках мы добавляем фактические элементы SVG, которые образуют столбец, то есть прямоугольник, его заголовок и его значение внутри column container. Прямоугольник столбца изначально имеет ширину 0, чтобы показать растущую анимацию, и имеет постоянную высоту, рассчитанную с использованием функции step нашего масштаба оси. Для заголовка столбца и его значения их атрибуты x и y также вычисляются с использованием функции step. В качестве последнего шага мы устанавливаем их значения innerText с помощью метода text. Заголовок столбца останется прежним, но мы установили значение столбца равным 0, чтобы можно было использовать функцию промежуточного кадра, которая будет объяснена в следующем разделе.

Обновить выделение

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

const barUpdate = barGroupsEnter.merge(barGroups);

В D3 есть merge метод соединения входных разделов с разделами обновления. Это просто даст нам выбор обновления. Используя barUpdate, мы будем использовать для заполнения обновленных значений.

В следующих строках кода мы устанавливаем фактические данные наших элементов (атрибуты или innerText). Более того, здесь мы используем transition функции для создания анимации. От значений, указанных в ввести выбор до обновить выбор, D3.js выполнит переход / анимацию за заданное время (см. Раздел «Функция рендеринга»). D3.js может самостоятельно вычислять интерполированные значения. Однако вы также можете определить свою собственную функцию интерполятора.

.tween("text", function({ value }) {
  const interpolateStartValue =
    elapsedTime === chartSettings.duration
      ? this.currentValue || 0
      : +this.innerHTML;
  const interpolate = d3.interpolate(interpolateStartValue, value);
  this.currentValue = value;
  return function(t) {
    d3.select(this).text(Math.ceil(interpolate(t)));
  };
});

Это не что иное, как tween функция для создания наших пользовательских интерполяций. Он принимает имя атрибута в качестве первого аргумента и функцию в качестве второго аргумента. Во втором аргументе вы можете определить свою собственную функцию интерполяции и вернуть ее. interpolate метод принимает два аргумента: начальное и конечное значение. И используя эти аргументы, он может заполнить соответствующее значение в заданное время. Под капотом функция возврата tween будет многократно вызываться во время фазы перехода, и, используя прошедшее время, t, вы можете оценить следующее значение, вызвав interpolate(t). Это пример того, как вы можете создать собственный интерполятор.

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

Выйти из выделения

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

const bodyExit = barGroups.exit();
bodyExit
  .transition(transition)
  .attr("transform", `translate(0,${innerHeight})`)
  .remove();

При выборе выхода мы решаем, что произойдет с элементами прямо перед выходом из данных, в основном их удаление из DOM. Например, для column-container элементов мы устанавливаем их атрибуты преобразования с помощью функции перехода, а затем удаляем элемент. При таком переходе наши столбцы сначала перейдут в нижнюю строку за счет уменьшения ширины и значения их прямоугольника. И когда они достигнут самого нижнего положения, они исчезнут и будут удалены из DOM.

На этом этапе мы можем рисовать наши столбцы и информацию о них, добавляя элементы rect и text. Фактически, мы можем успешно визуализировать нашу визуализацию столбчатой ​​диаграммы. Но мы хотим создать анимацию гонки на гистограмме, поэтому нам нужно последовательно вызывать функцию draw. Давайте посмотрим, как мы можем выполнить render функцию.

Функция рендеринга

Теперь мы можем рисовать наши компоненты в зависимости от передаваемых данных dataset. Однако в гонке с гистограммой мы последовательно визуализируем все разные наборы данных, chartDataSets. Перед визуализацией нашей визуализации мы берем набор (ы) данных, используя addDataset или addDatasets методы. В нашем случае каждый набор данных представляет собой данные за определенный год.

async function render() {
  for (const chartDataSet of chartDataSets) {
    chartTransition = chartContainer
      .transition()
      .duration(chartSettings.duration)
      .ease(d3.easeLinear);
    draw(chartDataSet, chartTransition);
    await chartTransition.end();
  }
}

Первое, что мы можем сделать для рендеринга наборов данных диаграммы, - это перебрать каждый из них и вызвать функцию draw. Давайте посмотрим, мы создаем переход, задавая функции длительность и легкость. Продолжительность определяет, как долго будет длиться анимация, а функция замедления определяет функцию замедления, которая будет использоваться для перехода. По этой ссылке вы можете просмотреть другие типы функций замедления. После создания объекта перехода мы передаем его функции рисования. Наконец, в функции рисования наша реализация будет работать с переходом, имеющим длительность. Итак, мы должны дождаться завершения нашего перехода, а затем мы сможем нарисовать следующий набор данных. Благодаря D3.js у нас появилась функция transition.end. Используя его, мы можем дождаться завершения текущей функции рисования.

Это простой способ последовательной визуализации всех наборов данных. Но в нашем случае мы хотим построить что-то более сложное. Мы хотим остановить или возобновить анимацию.

async function render(index = 0) {
  currentDataSetIndex = index;
  timerStart = d3.now();
  chartTransition = chartContainer
    .transition()
    .duration(elapsedTime)
    .ease(d3.easeLinear)
    .on("end", () => {
      if (index < chartDataSets.length) {
        elapsedTime = chartSettings.duration;
        render(index + 1);
      } else {
        d3.select("button").text("Play");
      }
    })
    .on("interrupt", () => {
      timerEnd = d3.now();
    });
  if (index < chartDataSets.length) {
    draw(chartDataSets[index], chartTransition);
  }
  return this;
}
function stop() {
  d3.select(`#${chartId}`)
    .selectAll("*")
    .interrupt();
  return this;
}
function start() {
  elapsedTime -= timerEnd - timerStart;
  render(currentDataSetIndex);
  return this;
}

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

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

Спасибо

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

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