Линейный график в D3.js строится на сочетании нескольких ключевых
компонентов: шкал (scales), генератора линии (line generator), осей
(axes) и SVG-контейнера. Основная идея заключается в преобразовании
массива данных в набор координат, которые затем интерпретируются как
путь (path) в SVG.
Типичная структура данных для одной серии:
const data = [
{ x: 0, y: 10 },
{ x: 1, y: 20 },
{ x: 2, y: 15 },
{ x: 3, y: 30 }
];
SVG-контейнер задаёт область визуализации:
const width = 800;
const height = 400;
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
const svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
Шкалы определяют соответствие между значениями данных и пикселями на экране.
const xScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.x))
.range([margin.left, width - margin.right]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.y)])
.nice()
.range([height - margin.bottom, margin.top]);
Ключевой момент: ось Y инвертируется, так как координатная система SVG начинается сверху.
Генератор линии преобразует данные в SVG-путь:
const line = d3.line()
.x(d => xScale(d.x))
.y(d => yScale(d.y));
Результат работы — строка d, используемая в
<path>.
svg.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 2)
.attr("d", line);
Оси добавляют читаемость и контекст:
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(xAxis);
svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(yAxis);
Односерийный график представляет один набор зависимых значений. Основная логика остаётся линейной: данные → шкалы → линия → отрисовка.
Полная сборка:
const line = d3.line()
.x(d => xScale(d.x))
.y(d => yScale(d.y));
svg.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 2)
.attr("d", line);
Для улучшения читаемости часто добавляются точки:
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", d => xScale(d.x))
.attr("cy", d => yScale(d.y))
.attr("r", 3)
.attr("fill", "steelblue");
Многосерийный линейный график требует иной структуры данных. Наиболее распространённый формат:
const data = [
{
name: "Серия A",
values: [
{ x: 0, y: 10 },
{ x: 1, y: 20 },
{ x: 2, y: 15 }
]
},
{
name: "Серия B",
values: [
{ x: 0, y: 5 },
{ x: 1, y: 25 },
{ x: 2, y: 30 }
]
}
];
Шкалы строятся на объединённых данных:
const allValues = data.flatMap(d => d.values);
const xScale = d3.scaleLinear()
.domain(d3.extent(allValues, d => d.x))
.range([margin.left, width - margin.right]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(allValues, d => d.y)])
.nice()
.range([height - margin.bottom, margin.top]);
Каждая серия превращается в отдельный path:
const line = d3.line()
.x(d => xScale(d.x))
.y(d => yScale(d.y));
svg.selectAll(".line")
.data(data)
.enter()
.append("path")
.attr("class", "line")
.attr("fill", "none")
.attr("stroke", (d, i) => d3.schemeCategory10[i])
.attr("stroke-width", 2)
.attr("d", d => line(d.values));
Для различения линий применяется цветовая шкала:
const color = d3.scaleOrdinal(d3.schemeCategory10);
Использование:
.attr("stroke", d => color(d.name))
Легенда связывает цвет и название серии:
const legend = svg.selectAll(".legend")
.data(data)
.enter()
.append("g")
.attr("transform", (d, i) => `translate(0,${i * 20})`);
legend.append("rect")
.attr("x", width - 20)
.attr("width", 10)
.attr("height", 10)
.attr("fill", d => color(d.name));
legend.append("text")
.attr("x", width - 25)
.attr("y", 10)
.attr("text-anchor", "end")
.text(d => d.name);
Плавное появление линии реализуется через
stroke-dasharray:
const path = svg.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 2)
.attr("d", line);
const totalLength = path.node().getTotalLength();
path
.attr("stroke-dasharray", `${totalLength} ${totalLength}`)
.attr("stroke-dashoffset", totalLength)
.transition()
.duration(1500)
.attr("stroke-dashoffset", 0);
D3.js использует принцип связывания данных с DOM. Для динамических графиков:
svg.selectAll(".line")
.data(data)
.join("path")
.attr("fill", "none")
.attr("stroke", d => color(d.name))
.attr("d", d => line(d.values));
Это обеспечивает корректное обновление без пересоздания всей сцены.
Линии могут прерываться при null значениях:
const line = d3.line()
.defined(d => d.y !== null)
.x(d => xScale(d.x))
.y(d => yScale(d.y));
Адаптивный график учитывает размеры контейнера:
function resize() {
const width = container.clientWidth;
xScale.range([margin.left, width - margin.right]);
svg.selectAll("path").attr("d", line);
}
При работе с тысячами точек применяются оптимизации:
Структурирование кода улучшает поддерживаемость:
function createLineChart({ svg, data, xScale, yScale, color }) {
const line = d3.line()
.x(d => xScale(d.x))
.y(d => yScale(d.y));
svg.selectAll(".line")
.data(data)
.join("path")
.attr("fill", "none")
.attr("stroke", d => color(d.name))
.attr("d", d => line(d.values));
}
Односерийный график характеризуется прямой привязкой массива значений к одной линии. Многосерийный требует вложенной структуры и дополнительного слоя абстракции для группировки данных.
Главное отличие проявляется в масштабировании доменов: при нескольких сериях учитывается весь набор значений, а не отдельная последовательность.
Все серии обязаны использовать одинаковые шкалы. Несоблюдение этого
правила приводит к визуальному искажению сравнения данных. В D3.js это
достигается едиными scaleLinear для всех
path.