Практика: альтернатива столбчатой диаграмме с накоплением
Мне очень понравилась альтернатива столбчатой диаграмме с накоплением, недавно показанная в блоге Flerlage Twins. Статья замечательно описывает совместную работу Sebastine Amede, Darragh Murray и Kevin Flerlage, а также рассказывает о людях, вдохновивших итоговое оформление. Это прекрасный способ сохранить идею соотношения части и целого и при этом значительно упростить сравнение отдельных сегментов.
Как обычно, увидев интересную визуализацию, я задаюсь вопросом, насколько легко её повторить в Power BI с помощью Deneb: мне нравится не только разрабатывать инструмент, но и пользоваться им. Я решил, что воспроизведение примера из статьи с помощью Vega-Lite станет интересным упражнением. К счастью, в Vega-Lite есть несколько каналов кодирования для каждого направления декартовых координат, что довольно сильно упрощает задачу.
Готовое решение
Вот как будет выглядеть результат:

Хотя Deneb предназначен для Power BI, я создам пример непосредственно в Vega-Lite. Так мы сможем видеть ход работы во встроенных диаграммах в этой статье и исследовать или изменять их в редакторе. Если вы предпочитаете возможности Deneb, такие как использование темы Power BI или форматирование чисел Power BI, возможно, понадобятся небольшие изменения. Они должны быть довольно простыми.
Если вы хотите сразу начать с готового решения, нужные файлы находятся в конце статьи.
Наши данные
Начнём с простого финансового набора данных, доступного для скачивания на MS Learn. Для примера я добавляю столбцы Country и Segment, а также меру $ Sales. Это даёт более подробную детализацию, чем обычно требуется для простой столбчатой диаграммы:
| Country | Segment | Sales |
|---|---|---|
| Canada | Channel Partners | 19752468 |
| Canada | Enterprise | 144752736 |
| Canada | Government | 428829833 |
| Canada | Midmarket | 21616940 |
| Canada | Small Business | 361203792 |
| France | Channel Partners | 15178124 |
| France | Enterprise | 150182723 |
| France | Government | 432792397 |
| France | Midmarket | 25395919 |
| France | Small Business | 274034856 |
| Germany | Channel Partners | 9807620 |
| Germany | Enterprise | 146362549 |
| Germany | Government | 307773485 |
| Germany | Midmarket | 12452545 |
| Germany | Small Business | 281729079 |
| Mexico | Channel Partners | 8887825 |
| Mexico | Enterprise | 130960356 |
| Mexico | Government | 413892255 |
| Mexico | Midmarket | 16428775 |
| Mexico | Small Business | 308830851 |
| United States of America | Channel Partners | 12059458 |
| United States of America | Enterprise | 166894624 |
| United States of America | Government | 362146299 |
| United States of America | Midmarket | 21890524 |
| United States of America | Small Business | 463176030 |
Так формируется набор данных визуального элемента Deneb. Я добавлю эти поля в роль данных Values, как и в любом другом проекте.
Продумываем оформление
В готовом решении есть графические элементы text для дополнительных подписей, но основную идею можно описать так:
- Добавить поле группировки верхнего уровня Country в канал кодирования
xи агрегировать меру Sales с помощьюsum. - Добавить поле группировки нижнего уровня Segment в канал кодирования
xOffset.
Все остальные элементы будут работать в этих рамках.
Шаг 1: группировка верхнего уровня
Вот как построить сводную столбчатую диаграмму для группировки верхнего уровня Country:
{
"data": { "name": "dataset" },
"layer": [
{
"encoding": { "y": { "field": "Sales", "aggregate": "sum" } },
"mark": { "type": "bar", "color": "#d4d4d4" }
}
],
"encoding": {
"x": {
"field": "Country",
"sort": { "field": "Sales", "op": "sum", "order": "descending" }
},
"y": { "type": "quantitative" }
}
}
- Обратите внимание: я уже учитываю описанный подход и использую
layer, а неmarkна верхнем уровне. - Поскольку канал
yимеет типquantitative, я задаю его на верхнем уровне. Внутри слоя добавляю каналyс агрегированием Sales, учитывая детализацию данных. Он наследуетtypeверхнего уровня, поэтому повторять его не нужно: Vega-Lite объединяет шкалу y для слоёв. - Канал
xбудет общим для всех слоёв, поэтому он также задан на верхнем уровне. Я сортирую значения по убыванию Sales, снова с агрегированием.
Пока диаграмма выглядит вполне обычно, как мы и ожидали:
Шаг 2: группировка нижнего уровня
Когда основные каналы x и y настроены, можно добавить второй слой. Он задаст смещение внутри каждого столбца на основе поля Segment:
{
"data": { "name": "dataset" },
"layer": [
... // Наш первый слой
{
"encoding": {
"y": { "field": "Sales" },
"xOffset": {
"field": "Segment",
"sort": { "field": "Sales", "order": "descending" },
"scale": { "padding": 0.2 }
},
"color": {
"field": "Segment",
"sort": {"field": "Sales", "order": "descending" }
}
},
"mark": { "type": "bar", "stroke": "white" }
}
],
"encoding": {
... // Исходные каналы кодирования
}
}
- В этом слое канал
yназначен полю Sales. - Поскольку мы находимся на самом подробном уровне данных, агрегирование не требуется.
- Канал
xOffsetназначен полю Segment с сортировкой по убыванию Sales. Так внутри каждого большого столбца появляются отдельные столбцы, использующие общий каналxверхнего уровня для соответствующей Country. Небольшойpaddingпомогает сделать промежутки между ними удобнее. - Канал
colorназначен полю Segment, что также создаёт легенду. Она сортируется по убыванию продаж, чтобы порядок соответствовал каналуxOffsetи каждый элемент легенды совпадал с порядком сегментов на диаграмме. - У каждого столбца белая обводка
stroke, чтобы улучшить контраст с фоновым столбцом.
Теперь видно, что основная задача решена: у нас получились «столбцы внутри столбцов».
Шаг 3: упрощаем область построения
Чтобы сделать оформление менее перегруженным, уберём лишние элементы. На шаге 4 мы добавим подписи, а пока поймём, сколько места есть в нашем распоряжении.
{
"title": { "text": "Продажи по странам и сегментам (млн $)", "anchor": "start" },
"view": { "stroke": "transparent" },
"data": { "name": "dataset" },
"layer": [
{
"encoding": { "y": { "field": "Sales", "aggregate": "sum" } },
"mark": { "type": "bar", "color": "#d4d4d4" }
},
{
"encoding": {
"y": { "field": "Sales" },
"xOffset": {
"field": "Segment",
"sort": { "field": "Sales", "order": "descending" },
"scale": { "padding": 0.2 }
},
"color": {
"field": "Segment",
"sort": { "field": "Sales", "order": "descending" },
"legend": { "orient": "top", "title": null }
}
},
"mark": { "type": "bar", "stroke": "white" }
}
],
"encoding": {
"x": {
"field": "Country",
"sort": { "field": "Sales", "op": "sum", "order": "descending" },
"axis": null
},
"y": { "type": "quantitative", "axis": null }
}
}
Изменения выделены в коде выше. Кратко мы сделали следующее:
- Добавили заголовок и закрепили его слева, в начале области.
- Задали прозрачную обводку для
view, чтобы вокруг области построения не было рамки. - Переместили
legendполя Segment наверх и убрали её заголовок как избыточный. - Убрали обе оси, задав
null, поскольку они не нужны.
Вот обновлённое оформление:
Шаг 4: подписи
В завершение добавим подписи к каждому столбцу с помощью графических элементов text. Подход с двумя слоями хорошо работает, поэтому используем вложенные слои: новые элементы унаследуют необходимые параметры существующего расположения.
{
"title": { "text": "Продажи по странам и сегментам (млн $)", "anchor": "start" },
"view": { "stroke": "transparent" },
"data": { "name": "dataset" },
"transform": [{ "calculate": "datum.Sales / 1000000", "as": "Sales_M" }],
"layer": [
{
"encoding": { "y": { "field": "Sales", "aggregate": "sum" } },
"layer": [
{ "mark": { "type": "bar", "color": "#d4d4d4" } },
{
"mark": { "type": "text", "baseline": "bottom", "dy": -15 },
"encoding": { "text": { "field": "Country" } }
},
{
"mark": { "type": "text", "baseline": "top", "dy": -15 },
"encoding": {
"text": { "field": "Sales_M", "aggregate": "sum", "format": ",d" }
}
}
]
},
{
"encoding": {
"y": { "field": "Sales" },
"xOffset": {
"field": "Segment",
"sort": { "field": "Sales", "order": "descending" },
"scale": { "padding": 0.2 }
}
},
"layer": [
{
"mark": { "type": "bar", "stroke": "white" },
"encoding": {
"color": {
"field": "Segment",
"sort": { "field": "Sales", "order": "descending" },
"legend": { "orient": "top", "title": null }
}
}
},
{
"mark": { "type": "text", "dy": -7.5 },
"encoding": { "text": { "field": "Sales_M", "format": ",d" } }
}
]
}
],
"encoding": {
"x": {
"field": "Country",
"sort": { "field": "Sales", "op": "sum", "order": "descending" },
"axis": null
},
"y": { "type": "quantitative", "axis": null }
}
}
Я снова выделил изменённые строки. Основные изменения:
Добавил
transform, чтобы вычислить меру Sales в миллионах и сохранить её в новом поле Sales_M для подписей. Можно было бы применить форматирование Power BI к Sales, но я стараюсь сохранить возможность повторить пример в Vega-Lite за пределами Power BI.Создал вложенный слой в первом слое для трёх элементов:
- Исходного
bar— серого столбца для поля Country. - Элемента
textдля Country, смещённого черезdyна -15 пикселей вверх. - Элемента
textдля агрегированного Sales_M, также смещённого черезdyна -15 пикселей вверх.
У обоих элементов
textпротивоположные значения свойстваbaseline. Поэтому при одинаковых позицииyиoffsetони располагаются соответственно выше и ниже вычисленной позиции y.- Исходного
Во втором слое применил аналогичный подход и создал вложенный слой для двух элементов:
bar— цветного столбца для Segment. Каналcolorперенесён в этот вложенный слой, чтобы элементыtextего не наследовали.- Элемента
textдля Sales_M, смещённого черезdyна -7.5 пикселя вверх.
Теперь визуализация выглядит так, как мы и планировали:
Подведём итоги
Я очень благодарен Sebastine, Darragh и Kevin за время, потраченное на замечательное исследование этого подхода, и за то, что они поделились приёмами с сообществом визуализации данных. Надеюсь, наша реализация достойно продолжает их работу и помогает повторить это оформление в других инструментах. Возможно, вы узнали немного больше о возможностях Vega-Lite и о том, как Deneb помогает получать подобные результаты в Power BI.
Как и обещал, вот файлы готового решения:
Спасибо, как всегда, за чтение!
DM-P