Основой любой диаграммы Ганта является корректно спроектированная структура данных. Каждая задача описывается как временной интервал с дополнительными атрибутами, необходимыми для визуализации и взаимодействия.
Типичная запись задачи включает:
Пример структуры:
const tasks = [
{
id: 1,
name: "Анализ требований",
start: new Date("2026-01-01"),
end: new Date("2026-01-05"),
group: "Проектирование",
progress: 100
},
{
id: 2,
name: "Разработка архитектуры",
start: new Date("2026-01-06"),
end: new Date("2026-01-12"),
group: "Проектирование",
progress: 70
}
];
При усложнении структуры вводится иерархия задач, где родительские элементы агрегируют дочерние, а временные диапазоны вычисляются автоматически как минимальный старт и максимальный конец среди потомков.
D3.js работает поверх SVG, что делает его удобным инструментом для построения временных диаграмм.
const margin = { top: 40, right: 30, bottom: 40, left: 150 };
const width = 1000 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
const svg = d3.select("body")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
SVG-группа используется как контейнер для всех элементов диаграммы, что упрощает управление смещениями и масштабированием.
Ключевой элемент диаграммы Ганта — временная шкала. Для этого
используется d3.scaleTime, позволяющая отображать даты на
координатной оси X.
const xScale = d3.scaleTime()
.domain([
d3.min(tasks, d => d.start),
d3.max(tasks, d => d.end)
])
.range([0, width]);
Ось времени:
const xAxis = d3.axisBottom(xScale)
.ticks(d3.timeDay.every(1));
svg.append("g")
.attr("transform", `translate(0, ${height})`)
.call(xAxis);
Для вертикальной оси используется категориальная шкала
d3.scaleBand, распределяющая задачи по строкам:
const yScale = d3.scaleBand()
.domain(tasks.map(d => d.name))
.range([0, height])
.padding(0.2);
Ось категорий:
const yAxis = d3.axisLeft(yScale);
svg.append("g")
.call(yAxis);
Основной визуальный элемент диаграммы — прямоугольники, отображающие интервалы времени.
svg.selectAll(".task")
.data(tasks)
.enter()
.append("rect")
.attr("class", "task")
.attr("x", d => xScale(d.start))
.attr("y", d => yScale(d.name))
.attr("width", d => xScale(d.end) - xScale(d.start))
.attr("height", yScale.bandwidth())
.attr("fill", "#4e79a7");
Каждый прямоугольник вычисляется на основе временного диапазона, где:
x — позиция начала задачиwidth — длительность задачи в пикселяхy — вертикальное положение категорииДля отображения прогресса поверх основной полосы добавляется внутренний прямоугольник.
svg.selectAll(".progress")
.data(tasks)
.enter()
.append("rect")
.attr("class", "progress")
.attr("x", d => xScale(d.start))
.attr("y", d => yScale(d.name))
.attr("width", d => (xScale(d.end) - xScale(d.start)) * (d.progress / 100))
.attr("height", yScale.bandwidth())
.attr("fill", "#59a14f");
Такое наложение позволяет визуально различать запланированное и выполненное время.
Для повышения читаемости используется группировка задач и цветовое разделение.
const color = d3.scaleOrdinal()
.domain([...new Set(tasks.map(d => d.group))])
.range(["#4e79a7", "#f28e2b", "#e15759"]);
Применение цвета:
.attr("fill", d => color(d.group))
Группировка особенно важна при большом количестве задач, позволяя визуально разделять этапы проекта.
Сетка улучшает ориентацию по временной оси и строится на основе той же шкалы времени.
const xGrid = d3.axisBottom(xScale)
.tickSize(-height)
.tickFormat("")
.ticks(d3.timeDay.every(1));
svg.append("g")
.attr("class", "grid")
.attr("transform", `translate(0, ${height})`)
.call(xGrid);
Линии сетки визуально связывают задачи с временными отметками.
Диаграмма Ганта часто включает связи между задачами, отображающие порядок выполнения.
Структура зависимости:
{
id: 2,
dependsOn: 1
}
Отрисовка линии зависимости:
svg.append("line")
.attr("x1", xScale(task1.end))
.attr("y1", yScale(task1.name) + yScale.bandwidth() / 2)
.attr("x2", xScale(task2.start))
.attr("y2", yScale(task2.name) + yScale.bandwidth() / 2)
.attr("stroke", "#999")
.attr("stroke-width", 1);
При большом количестве связей используются кривые Безье для предотвращения наложений:
const link = d3.linkHorizontal()
.x(d => xScale(d.date))
.y(d => yScale(d.task));
Интерактивные элементы повышают информативность диаграммы.
Простейший tooltip:
const tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("background", "#fff")
.style("border", "1px solid #ccc")
.style("padding", "6px")
.style("display", "none");
Обработка событий:
svg.selectAll(".task")
.on("mouseover", (event, d) => {
tooltip.style("display", "block")
.html(`${d.name}<br>${d.start} - ${d.end}`);
})
.on("mousemove", event => {
tooltip.style("left", event.pageX + 10 + "px")
.style("top", event.pageY + 10 + "px");
})
.on("mouseout", () => {
tooltip.style("display", "none");
});
Для работы с большими проектами применяется zoom-поведение.
const zoom = d3.zoom()
.scaleExtent([1, 5])
.on("zoom", (event) => {
svg.attr("transform", event.transform);
});
d3.select("svg").call(zoom);
Масштабирование влияет на временную шкалу и расположение задач, обеспечивая навигацию по длинным проектам.
Редактирование временных интервалов возможно через drag-события.
const drag = d3.drag()
.on("drag", (event, d) => {
const dx = xScale.invert(event.dx) - xScale.invert(0);
d.start = new Date(d.start.getTime() + dx);
d.end = new Date(d.end.getTime() + dx);
d3.select(event.sourceEvent.target)
.attr("x", xScale(d.start));
});
svg.selectAll(".task")
.call(drag);
Перетаскивание требует синхронизации данных и визуального слоя, иначе возникает рассогласование состояния.
При увеличении количества задач производительность становится критическим фактором.
Основные методы оптимизации:
join вместо enter/exit при
обновленииПример обновления через join:
svg.selectAll(".task")
.data(tasks, d => d.id)
.join(
enter => enter.append("rect"),
update => update,
exit => exit.remove()
);
При наличии вложенных задач применяется древовидная структура.
const root = d3.hierarchy(data);
root.each(d => {
if (d.children) {
d.start = d3.min(d.children, c => c.start);
d.end = d3.max(d.children, c => c.end);
}
});
Такой подход позволяет строить агрегированные уровни: этапы, фазы, подзадачи.
При изменении масштаба необходимо пересчитывать ось времени:
function updateAxis(transform) {
const newXScale = transform.rescaleX(xScale);
svg.select(".x-axis")
.call(d3.axisBottom(newXScale));
}
Это обеспечивает корректное отображение дат при zoom-операциях.
При тысячах задач используется виртуализация:
Фильтрация:
const visibleTasks = tasks.filter(d =>
d.end >= visibleStart && d.start <= visibleEnd
);
CSS позволяет отделить визуальную логику:
.task {
rx: 4;
ry: 4;
}
.grid line {
stroke: #eee;
}
Использование скруглений и тонких линий делает диаграмму визуально более читаемой при плотной компоновке.