Лепестковая диаграмма опирается на многомерные данные, где каждая ось соответствует отдельной метрике. Типичная структура включает набор объектов, каждый из которых описывает один «контур» диаграммы.
const data = [
{
name: "Модель A",
values: [
{ axis: "Скорость", value: 80 },
{ axis: "Надёжность", value: 90 },
{ axis: "Удобство", value: 70 },
{ axis: "Цена", value: 60 },
{ axis: "Функциональность", value: 85 }
]
}
];
Каждый объект values представляет собой пару:
axis — название измеренияvalue — числовое значениеДля корректного отображения данные приводятся к единой шкале.
Лепестковая диаграмма требует преобразования значений в радиальную
систему координат. Для этого используется масштабирование через
d3.scaleLinear.
const maxValue = 100;
const radiusScale = d3.scaleLinear()
.domain([0, maxValue])
.range([0, radius]);
Где radius — радиус окружности, в пределах которой
строится диаграмма.
Нормализация значений обеспечивает сопоставимость разных метрик.
Количество осей определяется числом параметров. Каждая ось равномерно распределяется по углу окружности.
const totalAxes = data[0].values.length;
const angleSlice = (Math.PI * 2) / totalAxes;
Угол каждой оси вычисляется как:
Для отображения точек используется преобразование полярных координат:
function getCoordinates(angle, value) {
const r = radiusScale(value);
return {
x: Math.cos(angle) * r,
y: Math.sin(angle) * r
};
}
Важно учитывать центр диаграммы:
const centerX = width / 2;
const centerY = height / 2;
И финальное позиционирование:
x: centerX + Math.cos(angle) * r
y: centerY + Math.sin(angle) * r
Каждая ось представляет линию от центра к краю окружности.
const axisGroup = svg.append("g")
.attr("class", "axes");
axisGroup.selectAll("line")
.data(data[0].values)
.enter()
.append("line")
.attr("x1", centerX)
.attr("y1", centerY)
.attr("x2", (d, i) =>
centerX + Math.cos(angleSlice * i) * radius
)
.attr("y2", (d, i) =>
centerY + Math.sin(angleSlice * i) * radius
)
.attr("stroke", "#999");
Каждая ось фиксирует направление измерения.
Текстовые метки размещаются на концах осей с небольшим смещением наружу.
axisGroup.selectAll(".axis-label")
.data(data[0].values)
.enter()
.append("text")
.attr("x", (d, i) =>
centerX + Math.cos(angleSlice * i) * (radius + 10)
)
.attr("y", (d, i) =>
centerY + Math.sin(angleSlice * i) * (radius + 10)
)
.text(d => d.axis)
.style("font-size", "12px")
.attr("text-anchor", "middle");
Для улучшения читаемости часто применяют условное выравнивание текста в зависимости от угла.
Основная фигура диаграммы формируется замкнутым многоугольником. Для
этого используется генератор линий d3.lineRadial или ручное
построение пути.
const radarLine = d3.lineRadial()
.radius(d => radiusScale(d.value))
.angle((d, i) => i * angleSlice);
Использование:
svg.append("path")
.datum(data[0].values)
.attr("d", radarLine)
.attr("transform", `translate(${centerX},${centerY})`)
.attr("fill", "rgba(0, 150, 255, 0.3)")
.attr("stroke", "#0077cc");
При необходимости более точного контроля используется ручное формирование координат.
const line = d3.line()
.x((d, i) =>
centerX + Math.cos(angleSlice * i) * radiusScale(d.value)
)
.y((d, i) =>
centerY + Math.sin(angleSlice * i) * radiusScale(d.value)
)
.curve(d3.curveLinearClosed);
Для повышения читаемости диаграммы добавляются маркеры значений.
svg.selectAll(".radar-point")
.data(data[0].values)
.enter()
.append("circle")
.attr("cx", (d, i) =>
centerX + Math.cos(angleSlice * i) * radiusScale(d.value)
)
.attr("cy", (d, i) =>
centerY + Math.sin(angleSlice * i) * radiusScale(d.value)
)
.attr("r", 3)
.attr("fill", "#0077cc");
Для сравнения нескольких наборов данных используется наложение контуров.
const color = d3.scaleOrdinal(d3.schemeCategory10);
svg.selectAll(".radar-wrapper")
.data(data)
.enter()
.append("path")
.attr("d", d => radarLine(d.values))
.attr("transform", `translate(${centerX},${centerY})`)
.attr("fill", d => color(d.name))
.attr("opacity", 0.2)
.attr("stroke", d => color(d.name));
Каждый слой представляет отдельный объект сравнения.
Для ориентации создаются концентрические многоугольники или окружности.
const levels = 5;
const gridGroup = svg.append("g")
.attr("class", "grid");
for (let level = 1; level <= levels; level++) {
const r = radius / levels * level;
gridGroup.append("circle")
.attr("cx", centerX)
.attr("cy", centerY)
.attr("r", r)
.attr("fill", "none")
.attr("stroke", "#ddd");
}
Сетка обеспечивает визуальную оценку относительных значений.
Для улучшения читаемости применяется корректировка выравнивания текста:
function getTextAnchor(angle) {
if (angle === 0 || angle === Math.PI * 2) return "middle";
if (angle < Math.PI) return "start";
return "end";
}
Плавное появление линий повышает восприятие структуры данных.
svg.selectAll("path")
.attr("stroke-dasharray", function() {
const length = this.getTotalLength();
return length + " " + length;
})
.attr("stroke-dashoffset", function() {
return this.getTotalLength();
})
.transition()
.duration(1000)
.attr("stroke-dashoffset", 0);
Изменение данных в реальном времени реализуется через обновление привязанных данных.
function updateRadar(newData) {
svg.selectAll("path")
.data(newData)
.attr("d", d => radarLine(d.values));
}
При изменении структуры требуется пересчёт масштабов и углов.
При динамическом наборе метрик пересчитывается угол:
angleSlice = (Math.PI * 2) / newData[0].values.length;
Все элементы, зависящие от угла, обновляются синхронно.
При больших наборах данных применяются следующие подходы:
path вместо множества линийtransition при частых
обновленияхДля различения серий данных применяется шкала цветов:
const colorScale = d3.scaleOrdinal()
.domain(data.map(d => d.name))
.range(["#1f77b4", "#ff7f0e", "#2ca02c"]);
Каждая серия получает собственный стиль заливки и обводки.
Диаграмма адаптируется под размер контейнера:
const radius = Math.min(width, height) / 2 - margin;
При изменении размеров пересчитываются:
Для отображения значений используется всплывающая подсказка.
.on("mouseover", (event, d) => {
tooltip
.style("opacity", 1)
.html(`${d.axis}: ${d.value}`);
})
.on("mousemove", event => {
tooltip
.style("left", event.pageX + "px")
.style("top", event.pageY + "px");
});
При большом разбросе значений форма может становиться неинформативной. Для стабилизации применяются:
const scale = d3.scaleLinear()
.domain([d3.min(values), d3.max(values)])
.nice();
Равномерность распределения осей обеспечивает визуальный баланс. При нечётных количествах метрик применяется поворот начальной оси для симметрии восприятия.