Недавно наша компания представила на публику новую версию продуктов, традиционно предложив большое количество контролов, фич и плюшек. Наша команда DevExtreme не стала исключением, и одним из результатов нашей плодотворной работы стал полярный график. Почему мы решили его сделать, с какими проблемами столкнулись, и что же у нас в итоге вышло?
Почему мы решили сделать полярный график?
Полярный график — неординарный тип диаграммы, используемый не так часто, как линейные, круговые диаграммы и гистограммы. Это графический способ отображения многомерных данных в виде двумерной диаграммы с тремя или более переменными, представленных на осях, начинающихся из единой точки. Полярный график часто называют радиальной диаграммой, лепестковой диаграммой, паутиной. Если говорить о полярном графике, в памяти сразу всплывает пример использования — это всем знакомая роза ветров.
Отображение данных в полярной системой координат
Графики с полярной системой координат являются одним из способов отображения данных, наряду с их отображением в прямоугольной системе координат. По существу, почти любой график в прямоугольной системе координат можно отобразить в полярной, и наоборот.
История полярного графика
Откуда же появился такой замысловатый тип графика?
Если подумать, все древние календари, изображающие время циклично по кругу, являются своеобразным примером представления данных в полярной системе координат. Знаки зодиака и созвездия тоже всегда изображали по кругу.
Если упоминать официальные документы, то в 1801 году Уильям Плейфер выпустил работу под названием “The Statistical Breviary”, в которой использовал некое подобие диаграммы в полярных координатах. Они не похожи на современное представление полярных графиков, скорее больше на круговые диаграммы, но являлись первым шагом на пути к полярному графику.
В 1843 году Леон Лаланн использовал полярный график для отображения розы ветров. Флоренс Найтингейл использовала в 1858 году подобие полярного графика для отображения информации о причинах смерти в армии.
Несмотря на все это, первенство в использовании полярного графика приписывают Джоржу фон Майеру, который применил его в 1877 году.
А в 1997 году Патрик Хоффман в своей работе по обработке данных вводит термин радиальной визуализации. С тех пор полярный график применяется во многих современных инфографиках.
Конечно же, это не полная история появления и использования этого графика, есть большое количество и других примеров. Такой график не мог не попасть в наше поле зрения, это одна из причин решения его реализовать.
Недостатки и достоинства
Как и все типы графиков, полярный график имеет свои недостатки и достоинства, которые очень подробно описаны вот в этой книге. К недостаткам можно отнести:
- линейные диаграммы и гистограммы визуально обрабатываются легче и быстрее
- практически любой полярный график можно отобразить в прямоугольной системе координат, что ускорит его восприятие
- колебания значений на полярном графике выглядят значительнее, чем есть на самом деле, из-за неправильной оценки площадей в полярной системе координат человеческим глазом
Но кроме недостатков у полярного графика есть и достоинства:
- в полярном графике используются графические примитивы — круги, и хоть такие графики сложнее воспринимать, чем линейные, их все же легче анализировать, чем графики с другими сложными формами
- полярный график позволяет быстро оценить симметрию значений
- полярные графики выглядят интересно, свежо и привлекательно
- анализ и оценка временных цикличных данных лучше проводится именно на полярном графике, а не на линейном
- данные, которые интуитивно понятны в круговой форме, эффектнее смотрятся на полярном графике
Как видно, полярный график может быть полезен при визуализации данных, что тоже склонило чашу весов в пользу разработки такого инструмента.
Расширение линейки виджетов
Мы год от года расширяем линейку наших визуализационных инструментов, стараясь следовать в ногу со временем. У нас уже есть такие виджеты, как
- dxChart
- dxPieChart
- dxCircularGauge
- dxLinearGauge
- dxBarGauge
- dxRangeSelector
- dxSparkline
- dxBullet
- dxVectorMap
Поэтому появление у нас виджета полярного графика было лишь делом времени.
Итак, какие же причины побудили нашу команду сделать полярный график:
- Отображение данных в полярной системе координат является таким же важным инструментом визуализации, как и их отображение в прямоугольной системе координат, ведь практически любой график можно перевести из одной системы в другую.
- Полярный график имеет насыщенную историю его использования.
- Несмотря на все недостатки, в некоторых случаях полярный график может быть полезен при визуализации данных.
- Мы стремимся расширять линейку наших визуализационных продуктов.
Какие проблемы встретились нам на пути?
В начале нашей работы над полярным графиком мы задумались, а каким же он должен быть? Первое требование, которое мы к нему выдвинули, стало его соответствие со всей нашей линейкой виджетов, а именно, чтобы он имел похожее API, узнаваемый внешний вид и такую же быструю скорость работы. Второе, чего бы мы хотели, это затратить наименьшее количество ресурсов, людей, времени и средств для его создания.
Чтобы достичь все эти цели, самым оптимальным решением стало написать новый виджет на основе виджета обычного графика dxChart, повторно используя его компоненты.
Такие компоненты, как всплывающая подсказка, легенда и заголовок, выглядят одинаково на любом виджете, поэтому их можно было использовать повторно без каких-либо изменений. А вот ось и серию необходимо было модифицировать, ведь теперь им предстояло отображаться в полярной системе координат. Эти компоненты выполняют две задачи — работают с данными, которые одинаковы для любого графика, и отрисовывают себя. Именно в отрисовке и кроется основное отличие.
Наравне с тем “как рисовать”, должен был измениться и аспект “где рисовать”. В виджете обычного графика у нас есть специальный модуль, который по принятым данным возвращает координаты на экране. Этот модуль мы называем транслятор. Очевидно, что нам предстояло создать новый, полярный транслятор, который бы умел работать в полярных координатах.
В итоге мы обозначили наши основные проблемы:
- отсутствие полярного транслятора
- ось и серия не умеют рисоваться в полярных координатах
- необходимость научить легенду, тултип и заголовок работать с новым виджетом
Решив эти проблемы, мы бы получили виджет с хорошо продуманной компонентной архитектурой, которая позволила бы эффективно решать возникающие проблемы и расширять при необходимости его функциональность. Такой виджет позволял бы создавать разные сложные графики просто и быстро.
Что же у нас получилось?
Итак, решив все проблемы, стоявшие у нас на пути, мы реализовали полярный график, создав виджет dxPolarChart, имеющий однородную структуру с нашими другими виджетами и гибкое API, позволяющее решать широкий круг задач, а также имеющий быструю скорость как работы, так и отрисовки.
Сейчас наш полярный график поддерживает такие типы серий, как:
- scatter
- line
- area
- bar
- stacked bar
Такой инструмент, как задание периода для оси аргументов, позволяет отображать цикличные данные, а сама ось аргументов может быть нарисована как кругом, так и многоугольником, что полезно при создании графика-паутины.
Давайте посмотрим, как с помощью нашего виджета реализовать основные сценарии применения полярного графика. Все примеры можно посмотреть и потрогать тут.
График сравнения двух объектов по некоторым критериям
На полярном графике весьма удобно оценивать характеристики объектов между собой. Давайте сравним две породы кошек по следующим критериям: вес, длина шерсти, здоровье, добродушность, игривость и интеллект.
Как получить такой график? Начнем по порядку, с данных и серий. Опишем используемые данные:
dataSource: [{
arg: "Вес",
cat1: 8,
cat2: 5
}, {
arg: "Длина шерсти",
cat1: 7,
cat2: 3
}, {
arg: "Здоровье",
cat1: 7,
cat2: 6
}, {
arg: "Добродушность",
cat1: 7,
cat2: 8
}, {
arg: "Игривость",
cat1: 6,
cat2: 8
}, {
arg: "Интеллект",
cat1: 8,
cat2: 5
}]
Поле cat1 соответствует информации о породе мейн-кун, а cat2 — британской короткошерстной. Опишем это в двух сериях:
series: [{
name: "Мейн-кун",
valueField: "cat1"
}, {
name: "Британская короткошерстная",
valueField: "cat2"
}]
Типы обеих серий — это области с обводкой, поэтому опишем эти свойства для всех серий:
commonSeriesSettings: {
type: "area",
border: {
visible: true
}
}
Поскольку оценка в нашем графике производится по десятибалльной шкале, добавим в шкалу значений все эти баллы:
valueAxis: {
categories: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
}
Ну вот, с данными разобрались, перейдем к остальным элементам. Включим всплывающую подсказку:
tooltip: {
enabled: true
}
Опишем двухстрочный заголовок:
title: "Оценка двух пород кошекn по десятибалльной шкале"
Легенду расположим под заголовком, и выровняем текст справа от маркеров серий:
legend: {
horizontalAlignment: "center",
itemTextPosition: "right"
}
И последний штрих, поскольку мы хотим сделать именно график-паутину, опишем свойство:
useSpiderWeb: true
options = {
title: "Оценка двух пород кошекn по десятибалльной шкале",
useSpiderWeb: true,
dataSource: [{
arg: "Вес",
cat1: 8,
cat2: 5
}, {
arg: "Длина шерсти",
cat1: 7,
cat2: 3
}, {
arg: "Здоровье",
cat1: 7,
cat2: 6
}, {
arg: "Добродушность",
cat1: 7,
cat2: 8
}, {
arg: "Игривость",
cat1: 6,
cat2: 8
}, {
arg: "Интеллект",
cat1: 8,
cat2: 5
}],
commonSeriesSettings: {
type: "area",
border: {
visible: true
}
},
series: [{
name: "Мейн-кун",
valueField: "cat1"
}, {
name: "Британская короткошерстная",
valueField: "cat2"
}],
valueAxis: {
categories: [1,2,3,4,5,6,7,8,9,10]
},
legend: {
horizontalAlignment: "center",
itemTextPosition: "right"
},
tooltip: {
enabled: true
}
}
График с временными цикличными данными
Когда нужно сравнить данные за некоторые повторяющиеся периоды времени, продуктивнее всего использовать именно полярный график. Допустим, нам нужно сравнить данные о продажах по первым двум кварталам года.
Опишем данные:
dataSource: [{
arg: 1,
val: 12000000
},{
arg: 2,
val: 14000000
},{
arg: 3,
val: 13000000
},{
arg: 4,
val: 18000000
},{
arg: 5,
val: 21000000
},{
arg: 6,
val: 16000000
}]
Серия здесь одна и имеет тип линию:
series: {
type: "line"
}
Напишем двухстрочный заголовок:
title: "Продажи за первыйn и второй кварталы"
И отключим легенду, для одной серии она здесь явно лишняя:
legend: {
visible: false
}
Разберемся с осями. Начнем с оси значений. Так как у нас данные содержит большие числа продаж, миллионы, если быть точнее, то представим наши подписи на осях в этом формате:
valueAxis: {
label: {
format: "millions"
}
}
Теперь ось аргументов. Прежде всего, зададим период, равный 3 месяцам, то есть одному кварталу. Далее зададим интервал для аргументов, потому что нам нужно показывать подписи только для месяцев. А чтобы подписи имели приписку “месяц”, закастомизим их.
argumentAxis: {
period: 3,
tickInterval: 1,
label: {
customizeText: function(label) {
return label.valueText + " месяц";
}
}
}
options = {
title: "Продажи за первыйn и второй кварталы",
dataSource: [{
arg: 1,
val: 12000000
},{
arg: 2,
val: 14000000
},{
arg: 3,
val: 13000000
},{
arg: 4,
val: 18000000
},{
arg: 5,
val: 21000000
},{
arg: 6,
val: 16000000
}],
series: {
type: "line"
},
argumentAxis: {
period: 3,
tickInterval: 1,
label: {
customizeText: function(label) {
return label.valueText + " месяц";
}
}
},
valueAxis: {
label: {
format: "millions"
}
},
legend: {
visible: false
}
}
График розы ветров
Отображение розы ветров — это классическая область применения полярного графика.
Опять же, начнем с данных и серий. Вот как выглядят данные:
dataSource: [{
arg: "С",
day: 8,
night: 6
},{
arg: "СВ",
day: 3,
night: 2
},{
arg: "В",
day: 1,
night: 1
},{
arg: "ЮВ",
day: 0,
night: 2
},{
arg: "Ю",
day: 4,
night: 3
},{
arg: "ЮЗ",
day: 2,
night: 1
},{
arg: "З",
day: 4,
night: 4
},{
arg: "СЗ",
day: 6,
night: 4
}]
Здесь у нас присутствуют две серии, одна из которых соответствует полю day — данным про ветер днем, а другая полю night — данным про ветер ночью. Также каждая серия будет иметь соответствующий цвет:
series: [{
name: "Количество дней",
valueField: "day",
color: "#FFD700"
}, {
name: "Количество ночей",
valueField: "night",
color: "#009ACD"
}]
Также каждая из этих серий имеет тип линию, и точки маленького размера:
commonSeriesSettings: {
type: "line",
point: {
size: 6
}
}
Напишем двухстрочный заголовок:
title: "Роза ветров в Москвеn за октябрь 2014"
Так как мы хотим, чтобы первый аргумент, север, находился именно сверху, укажем это в оси аргументов:
argumentAxis: {
firstPointOnStartAngle: true
}
Теперь опишем ось значений. Прежде всего, укажем, что не главные, минорные, отметки на оси имеют интервал в один день, так они выглядят гораздо нагляднее. Сделаем к подписям на оси приписку “дн”:
minorTickInterval: 1,
label: {
customizeText: function(label) {
return label.valueText + " дн";
}
}
Осталось указать информацию о штиле по дням и ночам. Для этого будем использовать обычные прерывистые линии, которым зададим их значение, соответствующий цвет и подпись “Штиль”:
constantLines: [{
value: 5,
color: "#FFD700",
dashStyle: "dash",
label: {
text: "Штиль",
font: {
color: "#FFD700"
}
}
}, {
value: 7,
color: "#009ACD",
dashStyle: "dash",
label: {
text: "Штиль",
font: {
color: "#009ACD"
}
}
}]
options = {
title: "Роза ветров в Москвеn за октябрь 2014",
dataSource: [{
arg: "С",
day: 8,
night: 6
},{
arg: "СВ",
day: 3,
night: 2
},{
arg: "В",
day: 1,
night: 1
},{
arg: "ЮВ",
day: 0,
night: 2
},{
arg: "Ю",
day: 4,
night: 3
},{
arg: "ЮЗ",
day: 2,
night: 1
},{
arg: "З",
day: 4,
night: 4
},{
arg: "СЗ",
day: 6,
night: 4
}],
commonSeriesSettings: {
type: "line",
point: {
size: 6
}
},
series: [{
name: "Количество дней",
valueField: "day",
color: "#FFD700"
}, {
name: "Количество ночей",
valueField: "night",
color: "#009ACD"
}],
argumentAxis: {
firstPointOnStartAngle: true
},
valueAxis: {
minorTickInterval: 1,
label: {
customizeText: function(label) {
return label.valueText + " дн";
}
},
constantLines: [{
value: 5,
color: "#FFD700",
dashStyle: "dash",
label: {
text: "Штиль",
font: {
color: "#FFD700"
}
}
}, {
value: 7,
color: "#009ACD",
dashStyle: "dash",
label: {
text: "Штиль",
font: {
color: "#009ACD"
}
}
}]
}
График сравнения количественных данных
Несмотря на то что сравнение количественных данных является прерогативой линейный графиков, полярный график тоже можно использовать ради этой цели. Например, сравним количество населения, мужчин и женщин, в разных странах.
Традиционно, начнем с данных и серий. Данные здесь выглядят вот так:
dataSource:[{
arg:"США",
usa_male: 134782000,
usa_female: 140786000
},{
arg: "Бразилия",
brazil_male: 85127000,
brazil_female: 87730000
},{
arg: "Россия",
rus_male: 68278000,
rus_female: 64750000
},{
arg: "Япония",
japan_male: 52387000,
japan_female: 64586000
},{
arg: "Германия",
ger_male: 40450000,
ger_female: 42344000
},{
arg: "Великобритания",
gb_male: 23486000,
gb_female: 30206000
}]
Зададим столько серий, сколько и секторов. Раскрасим их в соответствующие цвета, а также для удобства задания дальнейших опций, добавим сериям, отображающим данные по мужчинам, тег male.
series:[{
name: "Кол-во мужчин в США",
valueField: "usa_male",
color: "#9B30FF",
tag: "male"
},{
name: "Кол-во женщин в США",
valueField: "usa_female",
color: "#7D26CD"
},{
name: "Кол-во мужчин в Бразилии",
valueField: "brazil_male",
color: "#1E90FF",
tag: "male"
},{
name: "Кол-во женщин в Бразилии",
valueField: "brazil_female",
color: "#1874CD"
},{
name: "Кол-во мужчин в России",
valueField: "rus_male",
color: "#54FF9F",
tag: "male"
},{
name: "Кол-во женщин в России",
valueField: "rus_female",
color: "#43CD80"
},{
name: "Кол-во мужчин в Японии",
valueField: "japan_male",
color: "#FF6A6A",
tag: "male"
},{
name: "Кол-во женщин в Японии",
valueField: "japan_female",
color: "#CD5555"
},{
name: "Кол-во мужчин в Германии",
valueField: "ger_male",
color: "#FF7F24",
tag: "male"
},{
name: "Кол-во женщин в Германии",
valueField: "ger_female",
color: "#CD661D"
},{
name: "Кол-во мужчин в Великобритании",
valueField: "gb_male",
color: "#FFC125",
tag: "male"
},{
name: "Кол-во женщин в Великобритании",
valueField: "gb_female",
color: "#CD9B1D"
}]
Каждая из этих серий имеет тип stackedbar. Почти у всех у них включен лэйбл, показывающий название страны. Зададим эти опции для всех серий:
commonSeriesSettings: {
type: "stackedbar",
label: {
visible: true,
font: {
size: 14
},
customizeText: function(label){
return label.argumentText;
}
}
}
Теперь лейблы будут показываться у всех секторов, а нам они нужны только для верхних, которые отображают данные по женщинам. Вот тут-то и пригодится наш тег male, который мы добавили в серии. Отключим лэйблы у серий, у которых есть такой тег:
customizeLabel: function(label) {
if (label.series.tag === "male") {
return {
visible: false
}
}
}
Теперь зададим ширину секторам — угол, который занимает каждый сектор, чтобы между ними не было просвета:
equalBarWidth: {
width: 60
}
Опишем заголовок:
title: "Население в разных странах"
Отключим легенду:
legend: {
visible: false
}
Отключим все оси, ведь они нам здесь не нужны. Ось аргументов тогда имеет вид:
argumentAxis: {
visible: false,
label: {
visible: false
},
grid: {
visible: false
},
tick: {
visible: false
}
}
У оси значений еще и к тому же отключим верхние отступы с помощью опции valueMarginsEnabled:
valueAxis: {
valueMarginsEnabled: false,
visible: false,
label: {
visible: false
},
grid: {
visible: false
},
minorGrid: {
visible: false
}
}
Отлично, осталось разобраться только со всплывающей подсказкой tooltip. Включим его и покрасим в цвет точки, над которым он всплывает. Для этого воспользуемся опцией customizeTooltip. Поскольку население измеряется в миллионах, чтобы не показывать такие большие числа, применим к нему формат millions. Также опишем, что всплывающая подсказка содержит информацию о всех точках на аргументе, то есть, независимо от того, на мужчин или на женщин наведен указатель, информация будет выводится вся. Для этого применим опцию shared. Тогда опции всплывающей подсказки будут вот такие:
tooltip: {
enabled: true,
shared: true,
font: {
color: "#FFFFFF",
size: 14
},
format: "millions",
customizeTooltip: function(arg) {
return {
color: arg.point.getColor(),
borderColor: arg.point.getColor()
}
}
}
options = {
title: "Население в разных странах",
dataSource:[{
arg:"США",
usa_male: 134782000,
usa_female: 140786000
},{
arg: "Бразилия",
brazil_male: 85127000,
brazil_female: 87730000
},{
arg: "Россия",
rus_male: 68278000,
rus_female: 64750000
},{
arg: "Япония",
japan_male: 52387000,
japan_female: 64586000
},{
arg: "Германия",
ger_male: 40450000,
ger_female: 42344000
},{
arg: "Великобритания",
gb_male: 23486000,
gb_female: 30206000
}],
commonSeriesSettings: {
type: "stackedbar",
label: {
visible: true,
font: {
size: 14
},
customizeText: function(label){
return label.argumentText;
}
}
},
equalBarWidth: {
width: 60
},
series:[{
name: "Кол-во мужчин в США",
valueField: "usa_male",
color: "#9B30FF",
tag: "male"
},{
name: "Кол-во женщин в США",
valueField: "usa_female",
color: "#7D26CD"
},{
name: "Кол-во мужчин в Бразилии",
valueField: "brazil_male",
color: "#1E90FF",
tag: "male"
},{
name: "Кол-во женщин в Бразилии",
valueField: "brazil_female",
color: "#1874CD"
},{
name: "Кол-во мужчин в России",
valueField: "rus_male",
color: "#54FF9F",
tag: "male"
},{
name: "Кол-во женщин в России",
valueField: "rus_female",
color: "#43CD80"
},{
name: "Кол-во мужчин в Японии",
valueField: "japan_male",
color: "#FF6A6A",
tag: "male"
},{
name: "Кол-во женщин в Японии",
valueField: "japan_female",
color: "#CD5555"
},{
name: "Кол-во мужчин в Германии",
valueField: "ger_male",
color: "#FF7F24",
tag: "male"
},{
name: "Кол-во женщин в Германии",
valueField: "ger_female",
color: "#CD661D"
},{
name: "Кол-во мужчин в Великобритании",
valueField: "gb_male",
color: "#FFC125",
tag: "male"
},{
name: "Кол-во женщин в Великобритании",
valueField: "gb_female",
color: "#CD9B1D"
}],
legend: {
visible: false
},
argumentAxis: {
visible: false,
label: {
visible: false
},
grid: {
visible: false
},
tick: {
visible: false
}
},
valueAxis: {
valueMarginsEnabled: false,
visible: false,
label: {
visible: false
},
grid: {
visible: false
},
minorGrid: {
visible: false
}
},
customizeLabel: function(label) {
if (label.series.tag === "male") {
return {
visible: false
}
}
},
tooltip: {
enabled: true,
shared: true,
font: {
color: "#FFFFFF",
size: 14
},
format: "millions",
customizeTooltip: function(arg) {
return {
color: arg.point.getColor(),
borderColor: arg.point.getColor()
}
}
}
}
Больше примеров, демок и опций можно посмотреть в нашей демо-галерее.
Выводы
И вот в новом релизе, задумав, осуществив и решив все проблемы, мы представляем наш новый вид графика dxPolarChart. Он позволяет решать как специфичные задачи, свойственные именно для этого типа графика, так и обыденные задачи, заменив собой обычные типы графиков.
Мы постарались сделать этот виджет удобным в использовании, с привлекательным узнаваемым дизайном, с высокой скоростью работы и отрисовки.
Наша команда надеется, что полярный график расширит ваши возможности по визуализации данных, и позволит вам сделать свои продукты лучше и интереснее. Насколько хорошо получилось реализовать полярный график, решать вам, а мы всегда открыты к вашим пожеланиям и конструктивной критике.
Автор: tatyana_ryzh