Компонент ParallelCoordinates: конфигурация осей

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

Подобный подход особенно эффективен для анализа:

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

Каждая запись данных отображается в виде линии, проходящей через все оси.

Пример структуры данных:

const data = [
    {
        id: 'server-1',
        cpu: 78,
        memory: 64,
        network: 120,
        disk: 55
    },
    {
        id: 'server-2',
        cpu: 35,
        memory: 82,
        network: 95,
        disk: 40
    }
]

Подключение компонента

Для работы используется пакет:

npm install @nivo/parallel-coordinates

Базовый пример:

import { ResponsiveParallelCoordinates } from '@nivo/parallel-coordinates'

const MyChart = () => (
    <ResponsiveParallelCoordinates
        data={data}
        variables={[
            {
                id: 'cpu',
                value: 'cpu'
            },
            {
                id: 'memory',
                value: 'memory'
            },
            {
                id: 'network',
                value: 'network'
            },
            {
                id: 'disk',
                value: 'disk'
            }
        ]}
        margin={{ top: 50, right: 80, bottom: 50, left: 80 }}
    />
)

Конфигурация осей через variables

Главная настройка осей выполняется через массив variables.

Каждый объект массива описывает одну вертикальную ось.

Минимальная конфигурация:

variables={[
    {
        id: 'cpu',
        value: 'cpu'
    }
]}

Поле id

Идентификатор оси.

{
    id: 'cpu'
}

Используется:

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

Значение должно быть уникальным.


Поле value

Указывает, откуда брать значение из объекта данных.

{
    id: 'cpu',
    value: 'cpu'
}

Фактически:

item['cpu']

Можно использовать функцию:

{
    id: 'load',
    value: item => item.cpu * item.memory
}

Это позволяет:

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

Диапазон значений оси

Свойство min

Минимальное значение шкалы.

{
    id: 'cpu',
    value: 'cpu',
    min: 0
}

Свойство max

Максимальное значение.

{
    id: 'cpu',
    value: 'cpu',
    max: 100
}

Полная настройка:

{
    id: 'cpu',
    value: 'cpu',
    min: 0,
    max: 100
}

Автоматический диапазон

Если min и max не заданы, компонент вычисляет диапазон автоматически.

{
    id: 'temperature',
    value: 'temperature'
}

В этом случае:

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

Динамический диапазон

Можно вычислять диапазон программно.

{
    id: 'traffic',
    value: 'traffic',
    min: data => Math.min(...data.map(d => d.traffic)),
    max: data => Math.max(...data.map(d => d.traffic))
}

Это удобно для:

  • потоковых данных;
  • API-данных;
  • динамических дашбордов.

Настройка подписей осей

Свойство legend

Текстовая подпись оси.

{
    id: 'cpu',
    value: 'cpu',
    legend: 'CPU Usage'
}

Локализация подписей

variables={[
    {
        id: 'cpu',
        value: 'cpu',
        legend: 'Загрузка CPU'
    },
    {
        id: 'memory',
        value: 'memory',
        legend: 'Память'
    }
]}

Многострочные подписи

Можно использовать переносы:

{
    id: 'network',
    value: 'network',
    legend: 'Network\nTraffic'
}

Форматирование значений оси

Свойство tickFormat

Позволяет управлять отображением значений шкалы.

Пример процентов:

{
    id: 'cpu',
    value: 'cpu',
    tickFormat: value => `${value}%`
}

Форматирование валюты

{
    id: 'revenue',
    value: 'revenue',
    tickFormat: value => `$${value}`
}

Форматирование больших чисел

{
    id: 'requests',
    value: 'requests',
    tickFormat: value => `${value / 1000}k`
}

Настройка делений шкалы

Свойство ticksPosition

Определяет положение делений.

{
    id: 'cpu',
    value: 'cpu',
    ticksPosition: 'before'
}

Возможные значения:

  • before
  • after

Количество делений

{
    id: 'memory',
    value: 'memory',
    tickValues: 5
}

Явный список делений

{
    id: 'cpu',
    value: 'cpu',
    tickValues: [0, 25, 50, 75, 100]
}

Масштабирование осей

Линейная шкала

Используется по умолчанию.

{
    id: 'temperature',
    value: 'temperature'
}

Логарифмическая шкала

{
    id: 'traffic',
    value: 'traffic',
    scale: {
        type: 'log',
        base: 10
    }
}

Подходит для данных с большим разбросом значений.


Симметричная шкала

{
    id: 'balance',
    value: 'balance',
    min: -100,
    max: 100
}

Используется при наличии отрицательных значений.


Индивидуальная стилизация осей

Цвет оси

{
    id: 'cpu',
    value: 'cpu',
    color: '#ff0000'
}

Толщина оси

axisWidth={2}

Цвет делений

theme={{
    axis: {
        ticks: {
            line: {
                stroke: '#999'
            }
        }
    }
}}

Настройка легенд осей

Положение легенды

axisLegendPosition="start"

Варианты:

  • start
  • middle
  • end

Смещение легенды

axisLegendOffset={40}

Расстояние между осями

Компонент автоматически распределяет оси по ширине контейнера.

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

  • наложение текста;
  • слишком плотное расположение;
  • плохая читаемость.

Решение — увеличение ширины контейнера.

<div style={{ width: 1400, height: 500 }}>
    <ResponsiveParallelCoordinates />
</div>

Порядок отображения осей

Порядок определяется порядком объектов в variables.

variables={[
    {
        id: 'cpu',
        value: 'cpu'
    },
    {
        id: 'memory',
        value: 'memory'
    },
    {
        id: 'network',
        value: 'network'
    }
]}

Изменение порядка:

variables={[
    {
        id: 'network',
        value: 'network'
    },
    {
        id: 'cpu',
        value: 'cpu'
    },
    {
        id: 'memory',
        value: 'memory'
    }
]}

Скрытие отдельных осей

Ось можно исключить из конфигурации.

const visibleVariables = variables.filter(v => v.visible)

Пример:

[
    {
        id: 'cpu',
        visible: true
    },
    {
        id: 'debug',
        visible: false
    }
]

Адаптивные конфигурации осей

Упрощённый набор для мобильных устройств

const mobileVariables = [
    {
        id: 'cpu',
        value: 'cpu'
    },
    {
        id: 'memory',
        value: 'memory'
    }
]

Полный набор для desktop

const desktopVariables = [
    {
        id: 'cpu',
        value: 'cpu'
    },
    {
        id: 'memory',
        value: 'memory'
    },
    {
        id: 'network',
        value: 'network'
    },
    {
        id: 'disk',
        value: 'disk'
    },
    {
        id: 'temperature',
        value: 'temperature'
    }
]

Работа с отрицательными значениями

{
    id: 'profit',
    value: 'profit',
    min: -5000,
    max: 5000
}

Для корректного отображения рекомендуется:

  • задавать фиксированный диапазон;
  • использовать симметричные значения;
  • избегать автоматического min/max.

Конфигурация осей с единицами измерения

Температура

{
    id: 'temperature',
    value: 'temperature',
    tickFormat: value => `${value}°C`
}

Скорость

{
    id: 'speed',
    value: 'speed',
    tickFormat: value => `${value} km/h`
}

Память

{
    id: 'memory',
    value: 'memory',
    tickFormat: value => `${value} GB`
}

Использование функций форматирования D3

Nivo использует экосистему D3.

Можно подключить:

npm install d3-format

Пример:

import { format } from 'd3-format'

const currency = format('$,.2f')

Использование:

{
    id: 'revenue',
    value: 'revenue',
    tickFormat: currency
}

Кастомные подписи осей

Использование React-компонентов

const AxisLabel = ({ id }) => (
    <text>
        {id.toUpperCase()}
    </text>
)

Настройка темы осей

Полная конфигурация:

theme={{
    axis: {
        domain: {
            line: {
                stroke: '#555',
                strokeWidth: 2
            }
        },
        legend: {
            text: {
                fontSize: 14,
                fill: '#222'
            }
        },
        ticks: {
            line: {
                stroke: '#777',
                strokeWidth: 1
            },
            text: {
                fontSize: 11,
                fill: '#555'
            }
        }
    }
}}

Интерактивное изменение осей

Динамическое переключение параметров

const [variables, setVariables] = useState(initialVariables)

Изменение:

setVariables(prev =>
    prev.filter(v => v.id !== 'network')
)

Генерация осей из структуры данных

Автоматическое создание конфигурации:

const variables = Object.keys(data[0])
    .filter(key => key !== 'id')
    .map(key => ({
        id: key,
        value: key
    }))

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

При 20–50 осях возникают:

  • перегрузка SVG;
  • падение FPS;
  • ухудшение читаемости.

Оптимизации:

lineOpacity={0.2}
lineWidth={1}
isInteractive={false}

Практический пример полной конфигурации

import { ResponsiveParallelCoordinates } from '@nivo/parallel-coordinates'
import { format } from 'd3-format'

const percent = format('.0%')

const data = [
    {
        id: 'node-1',
        cpu: 0.72,
        memory: 0.55,
        network: 140,
        disk: 82
    },
    {
        id: 'node-2',
        cpu: 0.48,
        memory: 0.91,
        network: 95,
        disk: 40
    }
]

export const Chart = () => (
    <div style={{ height: 600 }}>
        <ResponsiveParallelCoordinates
            data={data}
            variables={[
                {
                    id: 'cpu',
                    value: 'cpu',
                    min: 0,
                    max: 1,
                    tickFormat: percent,
                    legend: 'CPU'
                },
                {
                    id: 'memory',
                    value: 'memory',
                    min: 0,
                    max: 1,
                    tickFormat: percent,
                    legend: 'Memory'
                },
                {
                    id: 'network',
                    value: 'network',
                    tickFormat: v => `${v} Mb/s`,
                    legend: 'Network'
                },
                {
                    id: 'disk',
                    value: 'disk',
                    min: 0,
                    max: 100,
                    tickFormat: v => `${v}%`,
                    legend: 'Disk'
                }
            ]}
            margin={{
                top: 60,
                right: 80,
                bottom: 50,
                left: 80
            }}
            axisLegendPosition="start"
            axisLegendOffset={50}
            lineOpacity={0.5}
            lineWidth={2}
            theme={{
                axis: {
                    legend: {
                        text: {
                            fontSize: 14,
                            fontWeight: 600
                        }
                    }
                }
            }}
        />
    </div>
)