Диаграмма Ганта

Модель данных для диаграммы Ганта

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

Типичная запись задачи включает:

  • уникальный идентификатор
  • название задачи
  • время начала
  • время окончания
  • уровень вложенности или группа
  • зависимости (опционально)
  • статус выполнения

Пример структуры:

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
  }
];

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


Подготовка SVG-контейнера

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 при обновлении
  • минимизация перерисовки осей
  • кэширование вычислений шкал
  • группировка DOM-операций

Пример обновления через 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;
}

Использование скруглений и тонких линий делает диаграмму визуально более читаемой при плотной компоновке.