Форматы иерархических данных для Sunburst

Визуализация Sunburst в Nivo опирается на строго иерархическую структуру данных. Основной принцип заключается в том, что каждый узел дерева может содержать дочерние элементы, а также числовое значение, которое используется для расчёта площади сегмента. Такая модель позволяет представлять сложные многослойные структуры — от организационных иерархий до распределения бюджета или аналитики пользовательских сегментов.

Базовая форма данных строится вокруг рекурсивного объекта, где каждый уровень описывается через children, а листовые узлы содержат value.

const data = {
  name: "root",
  children: [
    {
      name: "A",
      children: [
        { name: "A1", value: 10 },
        { name: "A2", value: 20 }
      ]
    },
    {
      name: "B",
      children: [
        { name: "B1", value: 15 },
        { name: "B2", value: 5 }
      ]
    }
  ]
};

Ключевой момент заключается в том, что value может присутствовать только у конечных узлов. Если значение указано у промежуточного узла, оно либо игнорируется, либо используется в зависимости от режима агрегации.


Рекурсивная структура и требования Nivo

Sunburst-компонент Nivo использует внутреннюю обработку через иерархические алгоритмы наподобие d3-hierarchy. Это накладывает несколько требований к структуре:

  • каждый узел должен иметь уникальное поле name или id
  • наличие массива children обязательно для узлов-родителей
  • листовые элементы обязаны содержать value
  • структура должна быть деревом без циклов

Пример корректной структуры с глубокой вложенностью:

const data = {
  name: "Market",
  children: [
    {
      name: "Electronics",
      children: [
        {
          name: "Phones",
          children: [
            { name: "iOS", value: 120 },
            { name: "Android", value: 300 }
          ]
        },
        {
          name: "Laptops",
          children: [
            { name: "Ultrabooks", value: 80 },
            { name: "Gaming", value: 60 }
          ]
        }
      ]
    }
  ]
};

Чем глубже структура, тем больше уровней отображается в Sunburst-диаграмме, где каждый слой соответствует одному уровню вложенности.


Плоские данные и необходимость трансформации

На практике данные часто поступают в плоском виде, например из API или аналитических систем. В таком случае требуется преобразование в древовидную структуру.

Пример плоского набора:

const flatData = [
  { category: "Electronics", sub: "Phones", type: "iOS", value: 120 },
  { category: "Electronics", sub: "Phones", type: "Android", value: 300 },
  { category: "Electronics", sub: "Laptops", type: "Gaming", value: 60 }
];

Для Sunburst необходимо преобразовать это в дерево:

function buildTree(data) {
  const root = { name: "root", children: [] };

  data.forEach(item => {
    let category = root.children.find(c => c.name === item.category);

    if (!category) {
      category = { name: item.category, children: [] };
      root.children.push(category);
    }

    let sub = category.children.find(s => s.name === item.sub);

    if (!sub) {
      sub = { name: item.sub, children: [] };
      category.children.push(sub);
    }

    sub.children.push({
      name: item.type,
      value: item.value
    });
  });

  return root;
}

Такая трансформация является обязательной, поскольку Sunburst не работает напрямую с табличными структурами.


Использование агрегируемых значений

Важной особенностью является автоматическая агрегация значений на промежуточных уровнях. Если у родительского узла отсутствует value, он вычисляется как сумма всех дочерних элементов.

{
  name: "A",
  children: [
    { name: "A1", value: 10 },
    { name: "A2", value: 20 }
  ]
}

В этом случае узел A получит суммарное значение 30.

Если же value задан вручную, поведение зависит от конфигурации компонента и может приводить к несоответствию визуализации.


Формат с явным значением узлов

Иногда требуется фиксировать значения на всех уровнях, включая промежуточные. Это используется при работе с заранее агрегированными данными.

const data = {
  name: "Total",
  value: 1000,
  children: [
    {
      name: "Segment A",
      value: 600,
      children: [
        { name: "A1", value: 400 },
        { name: "A2", value: 200 }
      ]
    },
    {
      name: "Segment B",
      value: 400,
      children: [
        { name: "B1", value: 250 },
        { name: "B2", value: 150 }
      ]
    }
  ]
};

Такой подход применяется в случаях, когда данные уже прошли этап агрегации на стороне backend.


Использование идентификаторов вместо имени

В сложных системах поле name не всегда является уникальным. Поэтому используется дополнительный идентификатор:

const data = {
  id: "root",
  children: [
    {
      id: "electronics",
      children: [
        { id: "phones_ios", value: 120, name: "iOS" },
        { id: "phones_android", value: 300, name: "Android" }
      ]
    }
  ]
};

Использование id повышает стабильность данных при динамических обновлениях, особенно при анимациях и перерендеринге.


Дерево с вычисляемыми путями (path-based структура)

В некоторых случаях данные приходят в виде путей:

const pathData = [
  { path: ["Electronics", "Phones", "iOS"], value: 120 },
  { path: ["Electronics", "Phones", "Android"], value: 300 }
];

Преобразование в дерево выполняется через поэтапное построение:

function buildFromPath(data) {
  const root = { name: "root", children: [] };

  const findOrCreate = (children, name) => {
    let node = children.find(n => n.name === name);
    if (!node) {
      node = { name, children: [] };
      children.push(node);
    }
    return node;
  };

  data.forEach(item => {
    let current = root;

    item.path.forEach((segment, index) => {
      if (index === item.path.length - 1) {
        const parent = findOrCreate(current.children, segment);
        parent.children = parent.children || [];
        parent.children.push({
          name: segment,
          value: item.value
        });
      } else {
        current = findOrCreate(current.children, segment);
      }
    });
  });

  return root;
}

Такой формат часто используется в логировании, файловых структурах и аналитике событий.


Нормализация данных перед передачей в Nivo

Перед использованием данных в Sunburst необходимо соблюдать несколько правил нормализации:

  • удаление пустых узлов без children и value
  • исключение null и undefined значений
  • приведение всех значений к числовому типу
  • обеспечение уникальности имён на одном уровне дерева

Пример фильтрации:

function clean(node) {
  if (node.children) {
    node.children = node.children
      .map(clean)
      .filter(Boolean);
  }

  if (!node.children || node.children.length === 0) {
    return node.value ? node : null;
  }

  return node;
}

Влияние структуры данных на визуализацию Sunburst

Форма данных напрямую определяет визуальное поведение диаграммы:

  • количество уровней дерева соответствует количеству концентрических кругов
  • распределение value определяет угловую ширину сегментов
  • глубина вложенности влияет на читаемость и плотность визуализации
  • несбалансированные деревья приводят к визуальной перегрузке отдельных секторов

Особенно важно учитывать баланс значений: если один узел значительно превышает остальные, визуальная структура становится доминированной одним сегментом, что снижает информативность.


Частые ошибки при подготовке данных

При работе с Sunburst часто возникают типовые ошибки структуры:

  • смешивание value и children без чёткой иерархии
  • отсутствие корневого узла
  • дублирование имён на одном уровне
  • передача массива вместо объекта верхнего уровня
  • использование строк вместо числовых значений

Пример некорректной структуры:

const broken = [
  { name: "A", value: 10 },
  { name: "B", children: [{ name: "B1", value: 5 }] }
];

Такая структура не будет интерпретирована как дерево и приведёт к ошибке рендеринга.


Подготовка данных для динамических интерфейсов

В интерактивных приложениях структура данных Sunburst часто изменяется в реальном времени. В таких случаях важны:

  • стабильные идентификаторы узлов
  • минимизация глубокой мутации объектов
  • использование иммутабельных обновлений дерева
  • предсказуемая структура при частичных обновлениях

Пример обновления значения без перестроения дерева:

function updateValue(node, id, newValue) {
  if (node.id === id) {
    return { ...node, value: newValue };
  }

  if (node.children) {
    return {
      ...node,
      children: node.children.map(child => updateValue(child, id, newValue))
    };
  }

  return node;
}

Оптимизация больших иерархий

При работе с большими наборами данных важна оптимизация структуры:

  • ограничение глубины вложенности
  • предварительная агрегация на сервере
  • кэширование вычисленных деревьев
  • удаление неиспользуемых веток

Глубокие структуры увеличивают нагрузку на вычисление layout и могут замедлять отрисовку Sunburst-компонента, особенно при анимациях переходов.