Несколько серий данных

Многие визуализации в Nivo основаны на работе с несколькими наборами данных, где каждая серия представляет отдельную линию, столбец или слой. Подход к организации таких данных определяет корректность отображения графика, поведение легенд, осей и интерактивных элементов.

Основной принцип многосерийной структуры заключается в разделении данных по ключам серий и унифицированной шкале значений по оси X.

Базовая модель данных

Для линейных графиков используется структура:

const data = [
  {
    id: 'Серия A',
    color: 'hsl(210, 70%, 50%)',
    data: [
      { x: 'Янв', y: 10 },
      { x: 'Фев', y: 20 },
      { x: 'Мар', y: 15 }
    ]
  },
  {
    id: 'Серия B',
    color: 'hsl(120, 70%, 50%)',
    data: [
      { x: 'Янв', y: 5 },
      { x: 'Фев', y: 12 },
      { x: 'Мар', y: 8 }
    ]
  }
]

Каждый объект верхнего уровня представляет отдельную серию. Поле id используется для легенд, подсказок и идентификации линии. Вложенный массив data определяет точки.


Многосерийная линейная диаграмма

Компонент ResponsiveLine из Nivo предназначен для отображения нескольких линий на одном графике.

import { ResponsiveLine } from '@nivo/line'

const MyChart = () => (
  <ResponsiveLine
    data={data}
    margin={{ top: 50, right: 110, bottom: 50, left: 60 }}
    xScale={{ type: 'point' }}
    yScale={{
      type: 'linear',
      min: 'auto',
      max: 'auto',
      stacked: false
    }}
    axisBottom={{
      tickSize: 5,
      tickPadding: 5,
      tickRotation: 0
    }}
    axisLeft={{
      tickSize: 5,
      tickPadding: 5,
      tickRotation: 0
    }}
    pointSize={10}
    pointBorderWidth={2}
    useMesh={true}
  />
)

Ключевой момент заключается в параметре stacked. При значении false серии отображаются независимо, при true происходит суммирование значений по оси Y.


Согласование шкалы X между сериями

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

Корректная нормализация:

const normalizedData = [
  {
    id: 'A',
    data: [
      { x: '2024-01', y: 30 },
      { x: '2024-02', y: 40 }
    ]
  },
  {
    id: 'B',
    data: [
      { x: '2024-01', y: 20 },
      { x: '2024-02', y: 25 }
    ]
  }
]

Нежелательная структура:

// разные X в сериях
[
  {
    id: 'A',
    data: [{ x: 'Jan', y: 10 }]
  },
  {
    id: 'B',
    data: [{ x: 'Feb', y: 20 }]
  }
]

Такая конфигурация нарушает сопоставление точек.


Стекирование серий

Стекированные графики позволяют отображать вклад каждой серии в общий итог.

<ResponsiveLine
  data={data}
  yScale={{
    type: 'linear',
    stacked: true
  }}
/>

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

Особенности поведения:

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

Многосерийные столбчатые диаграммы

Для категориальных данных используется ResponsiveBar.

Структура данных отличается: каждая строка представляет категорию, а ключи — серии.

const data = [
  {
    month: 'Янв',
    A: 10,
    B: 20
  },
  {
    month: 'Фев',
    A: 15,
    B: 25
  }
]

Конфигурация:

import { ResponsiveBar } from '@nivo/bar'

const MyBar = () => (
  <ResponsiveBar
    data={data}
    keys={['A', 'B']}
    indexBy="month"
    margin={{ top: 50, right: 130, bottom: 50, left: 60 }}
    padding={0.3}
    groupMode="grouped"
  />
)

Параметр keys определяет серии, которые будут отображаться как отдельные столбцы внутри категории.


Группировка и стек в Bar-чартах

Два основных режима:

grouped

Отображение серий рядом:

groupMode="grouped"

Характеристики:

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

stacked

groupMode="stacked"

Особенности:

  • серии складываются вертикально;
  • отражается суммарное значение;
  • используется для анализа вклада.

Передача динамических серий

Часто набор серий формируется динамически, например из API.

const keys = ['desktop', 'mobile', 'tablet']

const data = [
  { day: 'Mon', desktop: 120, mobile: 80, tablet: 30 },
  { day: 'Tue', desktop: 100, mobile: 90, tablet: 40 }
]

Передача:

<ResponsiveBar
  data={data}
  keys={keys}
  indexBy="day"
/>

Добавление новой серии не требует изменения структуры графика, только массива keys.


Многосерийные area-графики

Area-графики используют ту же структуру, что и line chart.

import { ResponsiveAreaBump } from '@nivo/bump'

или

import { ResponsiveArea } from '@nivo/area'

Пример:

<ResponsiveArea
  data={data}
  margin={{ top: 50, right: 110, bottom: 50, left: 60 }}
  xScale={{ type: 'point' }}
  yScale={{ type: 'linear', stacked: true }}
/>

Особенности многосерийного area:

  • перекрытие слоев зависит от порядка серий;
  • прозрачность критична для читаемости;
  • stacking усиливает визуальный эффект накопления.

Легенды при множестве серий

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

legends={[
  {
    anchor: 'bottom-right',
    direction: 'column',
    translateX: 100,
    itemWidth: 80,
    itemHeight: 20,
    symbolSize: 12
  }
]}

Каждый id серии автоматически отображается в легенде. Цвет можно задать явно или позволить системе распределить палитру.


Управление цветами серий

Цветовая схема может быть статической или динамической.

colors={{ scheme: 'nivo' }}

или

colors={(serie) => {
  if (serie.id === 'A') return '#ff0000'
  return '#00ff00'
}}

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

  • nivo
  • category10
  • set2

Tooltip в многосерийных данных

Tooltip получает доступ ко всей серии и конкретной точке:

tooltip={({ point }) => (
  <div>
    <strong>{point.seriesId}</strong>
    <div>X: {point.data.x}</div>
    <div>Y: {point.data.y}</div>
  </div>
)}

В случае Bar-чартов tooltip получает значение ключа внутри категории.


Фильтрация серий

Часто требуется скрытие отдельных серий.

const [hidden, setHidden] = useState(['B'])

const filteredData = data.map(serie => ({
  ...serie,
  data: hidden.includes(serie.id) ? [] : serie.data
}))

Более оптимальный подход — управление через легенду и события клика:

legends={[
  {
    onClick: (datum) => {
      toggleSerie(datum.id)
    }
  }
]}

Ошибки при работе с несколькими сериями

Несовпадающие ключи

В Bar-чартах отсутствие ключа в объекте приводит к нулевому значению.

{ month: 'Jan', A: 10 } // B отсутствует → 0

Несортированные X-значения

В Line-чартах несортированный x приводит к ломанию линии.

Разный тип шкалы

Смешение числовых и категориальных значений на X-оси нарушает интерполяцию.


Оптимизация большого количества серий

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

  • рост количества SVG-элементов;
  • увеличение нагрузки на tooltip;
  • сложность легенды.

Используются подходы:

  • агрегация данных до рендера;
  • виртуализация легенд;
  • ограничение количества одновременно отображаемых серий;
  • переход на canvas-рендер (при наличии).

Взаимодействие серий между собой

Некоторые компоненты позволяют анализировать корреляции между сериями:

  • сравнение трендов;
  • перекрытие областей;
  • относительные изменения.

При этом важно сохранять одинаковую временную или категориальную шкалу, иначе сравнение теряет смысл.


Динамическое добавление и удаление серий

setData(prev => [
  ...prev,
  {
    id: 'New Serie',
    data: [...]
  }
])

Удаление:

setData(prev => prev.filter(s => s.id !== 'A'))

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