Ключевые аспекты, необходимые для создания готового кода

В этой статье показано, как создавать реактивные диаграммы внутри приложения Angular 8 с использованием фреймворка D3 JavaScript.

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

Почему D3 поверх Angular?

Основная возможность D3 (Data-Driven-Documents) - управлять элементами DOM в ответ на динамические данные приложения. Но это касается Angular, React, jQuery и множества других фреймворков.

Мотивация к использованию инфраструктуры D3 (или аналогичной) с Angular заключается в предоставлении следующих возможностей визуализации данных:

  • Динамическое создание и удаление графических элементов из DOM.
  • Привязка данных приложения к графическим элементам.
  • Преобразование пользовательских данных в координаты диаграммы и формы.
  • Анимация графических элементов с использованием переходов и интерполяции.
  • Расширенные математические функции.

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

Сотрудничайте без конкуренции

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

Разработка компонентов для изоляции разделов DOM, управляемых Angular и D3, снижает некоторые риски.

Родительский компонент доставляет периферийные устройства диаграммы и размещает компонент диаграммы в одном выделенном контейнере.

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

Установка

Код и методы, описанные в этой статье, применимы к следующим версиям фреймворка.

  • Угловой 8.2.9
  • D3 5.12.0
  • Цель компилятора TypeScript: es2015

Просмотрите package.json и tsconfig.json в репозитории проекта для получения полного набора зависимостей и опций.

Предполагая, что вы уже создали приложение Angular 8, сделайте следующее в домашнем каталоге проекта:

npm install d3 --save
npm install @types/d3 --save-dev

Код

Создайте компонент диаграммы

Создайте компонент Angular для размещения функциональности диаграммы.

ng generate component app-area-chart

Импортируйте D3 в этот компонент.

import * as d3 from 'd3';

Принять входные данные

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

Предварительная обработка данных

Первым шагом в получении данных является их преобразование в параметры диаграммы / формы.

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

D3 предоставляет функции для этого для каждого типа диаграммы (d3.pie() , d3.histogram , d3.area() , и т. Д.).

// Histogram function to transform an array of numbers
// to a frequency distribution with 60 intervals
this.histogram = d3.histogram()
                   .value((datum) => datum)
                   .domain([0, 45])
                   .thresholds(this.x.ticks(60));
...
// data for this particular chart is a matrix with 4 rows
// each row contains lead times for each stage in pizza delivery
// first row for Preparation, second for Waiting, 
// third for In Transit and the last one for total delivery time
// Each row is processed and placed in an array of bins for 
// the frequency distribution.
private processData(data) {
    this.bins = [];
    data.forEach((row) => {
      this.bins.push(this.histogram(row))
    });
}

Создайте элементы диаграммы

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

...
// Set the color scale 
this.colorScale = d3.scaleOrdinal(d3.schemeCategory10);
// SVG element
this.svg = d3.select(this.hostElement).append('svg')
 .attr('width', '100%')
 .attr('height', '100%')
 .attr('viewBox', '0 0 ' + viewBoxWidth + ' ' + viewBoxHeight);
// Group element with origin at top left of SVG area
this.g = this.svg.append("g")
                .attr("transform", "translate(0,0)");

// X and Y Axis for area charts
this.x = d3.scaleLinear()
           .domain([0, 45])
           .range([30, 170]);
...
this.y = d3.scaleLinear()
           .domain([0, 200])
           .range([90, 10]);
//See code on github for formatting axis labels and ticks
...
// Area function to convert the frequency distributions
// to an area in the chart. Makes use of X and Y axis functions
// to transform the interval range and size to chart dimensions
this.area = d3.area()
              .x((datum: any) => this.x((datum.x0+datum.x1)/2))
              .y0(this.y(0))
              .y((datum: any) => this.y(datum.length);
...
// Create one area for each bin 
// - each bin represents the frequency distribution 
//   for each type of lead time
this.bins.forEach((row, index) => {
  this.paths.push(
      this.g.append('path')
            .datum(row)
            .attr('fill', this.colorScale('' + index))
            .attr("stroke-width", 0.1)
            .attr('opacity', 0.5)
            .attr('d', (datum: any) => this.area(datum))
      );
});

Сделайте это отзывчивым

Включите следующее, чтобы диаграмма реагировала на размеры и ориентацию устройства:

  • Используйте атрибут SVG viewBox при создании элемента SVG.
  • Укажите все размеры для элементов внутри SVG по отношению к viewBox ширине и высоте.
  • Убедитесь, что контейнер хоста в родительском компоненте реагирует.
// Another look at creation of SVG element 
// Use of viewBox ensures chart is responsive 
let viewBoxHeight = 100;
let viewBoxWidth = 200;
this.svg = d3.select(this.hostElement).append('svg')
 .attr('width', '100%')
 .attr('height', '100%')
 .attr('viewBox', '0 0 ' + viewBoxWidth + ' ' + viewBoxHeight);

Сделайте это реактивным

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

@Input() data: number[];
ngOnChanges(changes: SimpleChanges) {
  if(changes.data) {
    this.updateChart(changes.data.currentValue);
  }
}
updateChart(data: number[]) {
  //redirect to createChart if first call
}
createChart(data: number[]) {
  ...
}

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

this.paths.forEach((path, index) => {
  path.datum(this.bins[index])
      .transition()
      .duration(1000)
      .attr('d', (datum: any) => this.area(datum))
  );
});

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

Подробнее об анимации

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

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

// Pie function - transforms raw data to arc segment parameters
pie = d3.pie()
        .startAngle(-0.5 * Math.PI)
        .endAngle(0.5 * Math.PI)
        .sort(null)
        .value((d: number) => d);
...
this.pieData = this.pie(data);
...
// Arc generator
this.arc = d3.arc()
             .innerRadius(this.innerRadius)
             .outerRadius(this.radius);
// Add slices
this.slices = this.g.selectAll('allSlices')
                    .data(this.pieData)
                    .enter()
                    ...

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

// Rebind data to slices and enable transition to new dimensions
this.slices = this.slices.data(this.pieData);
this.slices.transition().duration(750)
           .attrTween("d", this.arcTween);
// Rebind data to labels and enable translation from 
// current centroid to the next centroid
this.labels.data(this.pieData);
this.labels.each((datum, index, n) => {
  d3.select(n[index])
    .text(this.labelValueFn(this.rawData[index]));
});
this.labels.transition()
  .duration(750)
  .attrTween("transform", this.labelTween);

Анимация круговых / кольцевых диаграмм требует «интерполяции». Учитывая данные формы для текущего и будущего состояния после обновления, «промежуточные» функции D3 помогают вычислить набор промежуточных наборов данных формы.

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

// Creates an "interpolator" for animated transition for arc slices.
//   Given previous and new arc states,
//   Generates a series of arcs 'tween the start and end states
arcTween = (datum, index) => {
        const interpolation = 
          d3.interpolate(this.pieDataPrevious[index], datum);
        this.pieDataPrevious[index] = interpolation(0);
        return (t) => {
            return this.arc(interpolation(t));
        }
    }

Функция анимации меток интерполирует промежуточные положения центроида на основе предыдущих и текущих значений необработанных данных.

// Creates an "interpolator" for animated transition for arc labels
//   Given previous and new label positions,
//   generates a series of centroids 'tween start and end state
labelTween = (datum, index) => {
  const interpolation = d3.interpolate(
     this.pieDataPrevious[index],
     datum
   );
   this.pieDataPrevious[index] = interpolation(0);
   return (t) => {
     return 'translate(' +
         this.arc.centroid(interpolation(t)) + ')';
}

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

Создайте родительский компонент

Разработайте родительский компонент с помощью Angular.

Не смешивать манипуляции с Angular и D3 DOM в одном компоненте (моя рекомендация) .

Храните периферийные устройства диаграммы (заголовок, легенды и таблицы данных) в этом родительском компоненте по следующим причинам:

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

Предоставьте контейнер в главном компоненте и внедрите компонент диаграммы как дочерний. Не помещайте в этот контейнер никаких других элементов.

<!--HTML-->
<div id="chartContainer">
   <app-area-chart #areaChart [data]="chartData">
   </app-area-chart>
</div>

Создайте дескриптор дочернего компонента, используя ViewChild. Убедитесь, что для ViewEncapsulation в дочернем компоненте установлено значение None, чтобы разрешить глобальные классы стилей для элементов SVG.

// Typescript
@Component({
  selector: 'app-order-delivery',
  templateUrl: './order-delivery.component.html',
  styleUrls: ['./order-delivery.component.scss']
})
export class OrderDeliveryComponent ... {

  @ViewChild('areaChart', {static: true})
  chart: AreaChartComponent;
  ...
}
...
@Component({
  selector: 'app-area-chart',
  encapsulation: ViewEncapsulation.None,
  templateUrl: './area-chart.component.html',
  styleUrls: ['./area-chart.component.scss']
})
export class AreaChartComponent ... {

Примечание. В предыдущей версии этой статьи декораторы Component для родительского OrderDeliveryComponent и дочернего AreaChartComponent смешивались. Атрибут ViewEncapsulation должен быть установлен в декораторе для дочернего компонента, как показано сейчас.

Стиль контейнера диаграммы

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

Если компонент диаграммы использует атрибут viewbox в элементе SVG и имеет 100% высоту и ширину, результирующая диаграмма автоматически масштабируется или сжимается в соответствии со своей родительской диаграммой.

// SCSS
#chartContainer {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  width: 100%;
  app-area-chart {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100%;
    width: 100%;
  }
}

Подготовьте данные и передайте их компоненту диаграммы

// Ensure the array is copied to a new array; Change detection will
// not fire in the child component for updates to an existing array in the parent component.
this.chart.data = [...this.chartData];

Заключение

D3 предоставляет мощный API для манипуляций с DOM. Использование его для визуализации данных в приложении Angular может значительно улучшить взаимодействие с пользователем.

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

использованная литература







И несколько других сообщений на Stack Overflow, bl.ocks.org и т. Д.