
Эволюционный подход в решении задач как нельзя кстати подходит для визуализации данных. Дивжение от простого к сложному, от одномерных данных к многомерным итерация за итерацией. В этой статье рассмотрим различные варианты круговых диаграмм, от самой простой одномерной до нестандартной самодельной многомерной. В качестве инструмента будем использовать D3.js. Всех заинтересованных прошу под кат.
Примечание: рядом с русскоязычными терминами в скобочках будет даваться англоязычный вариант.
Круговая столбчатая диаграмма
Кольцевая, круговоая, или радиальная столбчатая диаграмма (radial column/bar chart) является вариацией на тему классической столбчатой диаграммы (bar chart).
Окружность в качестве оси абсцисс (X) и концентрические окружности в роли координатной решетки, вот собственно отличительные особенности это вида графика от обычной столбчатой диаграммы. Выглядит это всё следующим образом.

Тут есть один нюанс, а именно какую шкалу (scale) использовать? Дело в том, что чем дальше от центра, тем больше будет длина внешней дуги (C = 2πR) и площадь нашего сектора (S ~ πR2). Поэтому нам нужно решить что для нас важнее: компенсировать различный визуальный вес наших столбцов или сохранить регулярность сетки.
Для первого варианта можно использовать масштаб, предложенный Майком Бостоком:
(function(global, factory) { typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("d3-scale")) : typeof define === "function" && define.amd ? define(["exports", "d3-scale"], factory) : (factory(global.d3 = global.d3 || {}, global.d3)); }(this, function(exports, d3Scale) { 'use strict'; function square(x) { return x * x; } function radial() { var linear = d3Scale.scaleLinear(); function scale(x) { return Math.sqrt(linear(x)); } scale.domain = function(_) { return arguments.length ? (linear.domain(_), scale) : linear.domain(); }; scale.nice = function(count) { return (linear.nice(count), scale); }; scale.range = function(_) { return arguments.length ? (linear.range(_.map(square)), scale) : linear.range().map(Math.sqrt); }; scale.ticks = linear.ticks; scale.tickFormat = linear.tickFormat; return scale; } exports.scaleRadial = radial; Object.defineProperty(exports, '__esModule', {value: true}); }));
Для регулярной сетки используйте стандартный линейный масштаб (d3.scaleLinear()). Я в своих примерах использовал именно его.
Постановка задачи
Некоторое время назад мне довелось пообщаться с представителями одного из подразделений Департамента транспорта Москвы, они то мне и предложили подумать над задачей визуализации данных по ДТП на МКАД. Предполагалось, что есть данные о месте и времени происшествия и скоростном режиме на участке дороги. Нужно было проверить есть ли связь между скоростным режимом и количеством аварий. Найти пространственные и временные паттерны (если они существуют). Первая задача решается с помощью диаграммы рассеяния (scatterplot) и построения регрессии. Вторая куда более креативная, ей то мы здесь и будем заниматься.
Да, никаких данных мне так и не предоставили, так что дальше пары эскизов и обсуждений дело не пошло.
Здесь и далее будем отображать «гипотетическое» (рандомное) количество аварий на МКАД за отчётный период. Ось абсцисс разбита на километровые отсечки и соответствующим образом повёрнута. Основная цель визуализации найти потенциальные пространственные и временные аномалии или паттерны. Да, нужно сохранить «топологичность» дороги, то есть закольцованность. Это красиво, и в данном случае вполне логично.
Две переменные одним махом
Поскольку движение на МКАД у нас двустороннее, то и статистику нам нужно отображать сразу по двум направлениям. Сделать это можно разными способами, рассмотрим их подробнее. Все графики кликабельны и ведут к исходникам на bl.ocks.org. Каждый вариант диграммы сопровождается комментарием о её читабельности*. Некоторые недостатки графиков можно нивелировать с помощью интерактива: различные ховер эффекты, фокусировки и др… Но мы здесь будем рассматривать возможности графиков только как статичных картинок.
* читабельность графика — совокупность свойств графика, определяющих скорость и полноту восприятия информации, отображённой на графике.
Круговая столбчатая диаграмма с группировкой
Пожалуй первое, что приходит в голову, это сгруппировать показатели.

Читабельность:
Оценить картину в целом довольно сложно, из-за близости столбцов их легко сравнивать, но сложно оценить два тренда разом.
Две круговые столбчатые диаграммы
Попробуем разнести наши дороги и данные в пространстве.
Читабельность:
Здесь просто два графика. Тренды легко отслеживаются. Попарное сравнение тоже работает, но хуже. Так бывает, одно лечим, другое калечим.
Круговая накопительная столбчатая диаграмма
Снова попробуем объединить показтели, теперь в виде накопительной диаграммы.

Вариант накопительной диаграммы вполне возможен. Но накопительные диаграммы не зря так называются, они хороши когда нам важно одновременно видеть показатель целиком и его составные части. У нас же немного другая ситуация. Так же такой тип графика не работает на большом количестве столбцов, а у нас их много. Получаем просто картинку с красивым «забором».
Читабельность:
Попарное сравнение есть, суммарное значение есть (хоть и не нужно), а вот отследить тренд для второго показателя весьма сложно из-за разного начального уровня.
Круговая накопительная расходящаяся столбчатая диаграмма
Вариант накопительный диаграммы с отрицательными значениями. У нас их нет, но нам важно, что база у столбцов здесь общая. Меняем знак у одной из колонок, затем создаём накопительную диаграмму со смещением: stack.offset([offset]). В качестве функции смещения передаём d3.stackOffsetDiverging.

Читабельность:
Попарное сравнение есть. Внешний тренд просматривается, внутренний тоже, но хуже. Картина в целом теперь есть, хотя просматривается ещё не чётко.
Круговая накопительная оппозитная столбчатая диаграмма
Название авторское, как собственно и график (возможно я не первопроходец, а просто плохо искал). Здесь я решил как бы инвертировать предыдущий вариант графика. Получилось вот что.

Данный график основан на принципах гештальта.
На принципе замыкания: наш глаз пытается замкнуть «открытые» фигуры.

На принципе фон-фигура.

С одной стороны меньшая из фигур воспринимается как главный график, с другой выпуклый график превалирует над вогнутым. Это создаёт нестабильность, но за счёт цвета мы можем ей управлять.
С точки зрения кода оснонвое отличие в собственной функции сдвига, в данном случае она выглядит так:
function stackOffsetOpposing(series, order) { // Check if no staks (amount of series < 1) if (!((n = series.length) > 1)) return; // find max sum var stackSums = []; var stackMaxes = []; for (var i = 0, n = series.length; i < n; i++) { var stackMax = d3.max(series[i], function(d) { return Math.abs(d[1] - d[0])}); stackMaxes.push(stackMax); } var max = d3.sum(stackMaxes); // Redifining baselines for (var i, j = 0, d, dy, yp, yn, n, m = series[order[0]].length; j < m; ++j) { for (yp = 0, yn = max, i = 0; i < n; ++i) { if ((dy = (d = series[order[i]][j])[1] - d[0]) >= 0) { //d[0] -bottom; d[1] -top d[0] = yp, d[1] = yp += dy; } else if (dy < 0) { d[1] = yn; yn += dy; d[0] = yn; } else { d[0] = yp; } } } }
Читабельность:
Попарное сравнение есть. Оба тренда просматриваются. Есть картина в целом.
Да, график остаётся накопительным, так что можно добавлять подкатегории.

Преимущества:
- хорошо видны данные в противофазе
- нет необходимости далеко переводить взгляд, не теряется контекст
- видно оба тренда
- суммарная картина по «пустому» графику
Временные кольца
По аналогии с годовыми кольцами деревьев можно добавлять кольца с данными за разные отчётные периоды. Главное не переусердствовать и правильно подобрать ширину колец. Выглядеть это может так.

Неплохой вариант для печати в большом формате, на экране, по-моему, смотрится довольно-таки тяжеловесно.
Заключение
Мы рассмотрели различные варианты круговых диаграмм, их плюсы и минусы. Несмотря на то, что этот класс диаграмм далеко не самый лучший в плане читаемости, он хорош в плане визуальной привлекательности. Так что если привлечь внимание «смотрящего» для вас важнее, чем скорость выуживания полезной информации, то такой вариант может вполне подойти. Надеюсь, рассмотренные примеры будут вам полезны в реальной работе. Да, JS код не соответствует современным стандартам, но он должен быть понятен. В любом случае это лишь примеры и заготовки для реальных визуализаций.
И на последок
Если бы были реальные данные, то можно было бы добавить интерактив, прикрутить фильтрацию и т.д… А поскольку их нет, да и статья обучающая, остановимся на этом. Но если вдруг кто-нибудь (ЦОДД, Яндекс?) предоставит сэмпл с реальными данными, то можно было бы продолжить изыскания.
А какой тип график предложили бы вы? Собственные варианты пишите в комментариях =)

