Как сделать простой PieChart на D3.js
Любите ли вы пайчарты? Я вот не очень, но они регулярно мне встречаются и иногда очень даже симпатичные и информативные. Поэтому сегодня я решила написать про них небольшую заметку и разобрать, как сделать простой пайчарт с помощью D3.
Немного истории
Первый пайчарт нарисовал Уильям Плейфэр в своей книге «Краткое изложение статистики» ("Statistical Breviary") ещё в 1801 году. Та визуализация показывала владения Османской империи в разных частях света.
Новый график не был очень популярен в то время, а сам Плейфэр считал, что ему не хватает третьего измерения, негде поместить дополнительную информацию.
Позже, в 1858 году Флоренс Найтингейл переиспользовала пайчарт, сделав визуализацию причин смерти солдат во время Крымской войны.
Чем плохи и хороши пайчарты
Если говорить честно, я не люблю пайчарты. Соглашусь тут с Тафти, что они используют избыточное количество чернил для отображения очень простых данных (хотя я и не всегда согласна с идеей Тафти о максимальном упрощении всего).
Но главная проблема пайчартов не в этом. Она в том, что они используют площадь, то есть двумерную величину. Мы легко можем сказать, какой из элементов находится выше или ниже, какой длиннее или короче, но нам сложно определить, какой элемент больше по площади.
Например, эти прямоугольники разные (не все), но сложно быстро расположить их по убыванию их размера:
Похожая проблема возникает и с секторами круга: понять, какой сектор больше, можно, сравнив длины дуг окружности, а вот определить, насколько он больше, становится сложнее. Особенно это заметно, когда сектора по-разному повёрнуты.
Но не всё с пайчартами плохо. Их вполне можно эффективно использовать, когда хочется показать отношение небольшого количества величин друг к другу и к их сумме.
Как сделать простой пайчарт на D3
Сейчас расскажу, как сделать самый обычный пайчарт на D3.
В качестве данных я взяла площади континентов (в миллионах квадратных километров):
const data = { Asia: 44.6, Africa: 30, North America: 24.5, South America: 17.8, Antarctica: 14.2, Europe: 9.9, Australia: 7.7, }
Сначала подготовим svg для рисования:
const size = 300; const radius = size / 2; const svg = d3.select('svg') .attr('width', size) .attr('height', size); const g = svg.append('g') .attr('transform', `translate(${radius}, ${radius})`);
Transform на половину размера нужен, чтобы сегменты отрисовывались из середины всей области, а не из верхнего левого угла.
const pie = d3.pie(); const pieData = pie(Object.values(data));
d3.pie — это специальный метод, который по массиву значений вычисляет все параметры сегментов круга, в том числе их начальные и конечные углы. Его код можно посмотреть здесь — весь класс занимает всего 80 строчек.
В результате получаем данные такого вида:
[1]: { index: 1, data: 30, value: 30, padAngle: 0, startAngle: 1.8845330511110263, endAngle: 3.152156179661044, }
Пайчарт состоит из сегментов, то есть закрашенных дуг окружности. Дуги это простые path элементы. Создадим их и привяжем к ним данные:
const arcs = g.selectAll('.arc') .data(pieData) .join(enter => enter .append('path') .attr('class', 'arc') );
Теперь можно рисовать. Сначала нам нужны палитра и функция для рисования дуг (arc). По смыслу и действию arc() похожа на line(): она умеет строить линию по элементу массива, подготовленного методом pie(). Для этого ей нужны 2 значения: внутренний и внешний радиус. Кстати, если внутренний радиус сделать не нулевым, то вместо пайчарта будет донат.
Когда всё готово, мы можем всем дугам (arcs) присвоить атрибуты формы (d) и цвета (fill):
const color = d3.scaleOrdinal(d3.schemeCategory10); const arc = d3.arc() .outerRadius(radius - 10) .innerRadius(0); arcs .attr('d', arc) .style('fill', (_, i) => color(i));
Получился вполне неплохой пайчарт. Но в нём ничего не понятно. Что значат цвета, какие значения у какого из сегментов? Можно, конечно, добавить легенду, но я покажу, как подписать каждый из элементов на самом графике.
Чтобы определить, где расположить подписи, создадим ещё одну функцию arc. Так как сами дуги рисовать мы не будем, можно сделать одинаковые радиусы где-то около границ, но так, чтобы текст точно попадал внутрь:
const label = d3.arc() .outerRadius(radius - 25) .innerRadius(radius - 25);
И после уже добавим текстовые элементы, сдвинув их в нужные позиции. Сдвинуть можно с помощью метода centroid, который получает данные о дуге и возвращает массив из двух координат — её центр:
const texts = g.selectAll('text') .data(pieData) .join(enter => enter .append('text') .attr('transform', d => `translate(${label.centroid(d)})`) .text(d => `${Object.keys(data)[d.index]}: ${d.value}`));