Практика: альтернатива столбчатой диаграмме с накоплением

Daniel Marsh-Patrick
Daniel Marsh-PatrickРуководитель программы и разработчик

Мне очень понравилась альтернатива столбчатой диаграмме с накоплением, недавно показанная в блоге 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 для дополнительных подписей, но основную идею можно описать так:

  1. Добавить поле группировки верхнего уровня Country в канал кодирования x и агрегировать меру Sales с помощью sum.
  2. Добавить поле группировки нижнего уровня 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