Решения

Как переключать грид/диаграмму

При использовании стандартной конфигурации макета, переключение грида или диаграммы выполняется изменением параметров show_grid или show_chart, после чего необходимо вызвать метод render() для обновления отображения.

function toggleGrid(){
    gantt.config.show_grid = !gantt.config.show_grid;
    gantt.render();
}

Related sample:  Gantt. Переключение грида (стандартный макет)

function toggleChart(){
    gantt.config.show_chart = !gantt.config.show_chart;
    gantt.render();
}

Related sample:  Gantt. Переключение временной шкалы (стандартный макет)

Для пользовательских макетов необходимо создать отдельные макеты с гридом и без него или с временной шкалой и без неё. Переключение между ними осуществляется обновлением параметра gantt.config.layout и инициализацией через метод init():

let showGrid = true;
function toggleGrid() {
    showGrid = !showGrid;
    if (showGrid) {
        gantt.config.layout = gridAndChart; // макет с гридом и временной шкалой
    }
    else {
        gantt.config.layout = onlyChart; // макет только с временной шкалой
 
    }
    gantt.init("gantt_here");
}

Related sample:  Gantt. Переключение грида (пользовательский макет)

let showChart = true;
function toggleChart() {
    showChart = !showChart;
    if (showChart) {
        gantt.config.layout = gridAndChart; // макет с гридом и временной шкалой
    }
    else {
        gantt.config.layout = onlyGrid; // макет только с гридом
 
    }
    gantt.init("gantt_here");
}

Related sample:  Gantt. Переключение временной шкалы (пользовательский макет)

Как переключать представление ресурсов

Аналогично переключению грида или временной шкалы, необходимо подготовить несколько макетов с ресурсами и без них. Для переключения между ними обновите параметр gantt.config.layout и вызовите метод init() для применения изменений:

let resourceChart = true;
 
function layoutChange() {
    resourceChart = !resourceChart;
    if (resourceChart) {
        gantt.config.layout = resourceLayout;
    }
    else {
        gantt.config.layout = noresourceLayout;
    }
    gantt.init("gantt_here");
};

Related sample:  Gantt. Переключение диаграммы загрузки ресурсов

let histogramView = true;
 
function layoutChange() {
    histogramView = !histogramView;
    if (histogramView) {
        gantt.config.layout = histogramLayout;
    }
    else {
        gantt.config.layout = simpleLayout;
    }
    gantt.init("gantt_here");
};

Related sample:  Gantt. Переключение гистограммы ресурсов

Другой подход — динамическое формирование макета с использованием layout views и повторная инициализация Gantt для обновления отображения:

Related sample:  Gantt. Генерация макета

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

Бесконечная прокрутка может быть реализована разными способами, но обычно это связано с изменением отображаемого диапазона дат через параметры gantt.config.start_date и gantt.config.end_date:

При использовании полосы прокрутки

Отслеживая позицию прокрутки, можно расширять диапазон дат при приближении пользователя к краям шкалы. Чтобы избежать проблем с производительностью, перерисовку Gantt рекомендуется выполнять с задержкой через timeout:

gantt.init("gantt_here");
gantt.parse(tasks);
 
gantt.attachEvent("onGanttScroll", function (left, top) {
    const left_date = gantt.dateFromPos(left)
    const right_date = gantt.dateFromPos(left + gantt.$task.offsetWidth)
 
    gantt.config.start_date = gantt.config.start_date || gantt.getState().min_date;
    gantt.config.end_date = gantt.config.end_date || gantt.getState().max_date;
 
    const min_allowed_date = gantt.date.add(gantt.config.start_date, 1, "day");
    const max_allowed_date = gantt.date.add(gantt.config.end_date, -2, "day");
 
    let repaint = false;
    if (+left_date <= +min_allowed_date) {
        gantt.config.start_date = gantt.date.add(gantt.config.start_date, -2, "day");
        repaint = true;
    }
    if (+right_date >= +max_allowed_date) {
        gantt.config.end_date = gantt.date.add(gantt.config.end_date, 2, "day");
        repaint = true;
    }
 
    if (repaint) {
        setTimeout(function () {
            gantt.render()
            gantt.showDate(left_date)
        }, 20)
    }
});

Related sample:  Gantt. Бесконечная прокрутка с полосой прокрутки

При перетаскивании временной шкалы

Определяя текущую позицию прокрутки при перетаскивании временной шкалы, можно расширять диапазон дат, если прокрутка приближается к началу или концу шкалы:

gantt.attachEvent("onMouseMove", function (id, e) {
  if (!gantt.getState().drag_id && e.buttons == 1) {
    const left_date = gantt.dateFromPos(gantt.getScrollState().x);
    const right_date = gantt.dateFromPos(
      gantt.getScrollState().x + gantt.$task.offsetWidth - 1
    );
    if (left_date && +left_date <= +gantt.config.start_date) {
      gantt.config.start_date = gantt.date.add(gantt.config.start_date, -1, 'day');
      gantt.render();
    }
    if (right_date && +gantt.config.end_date < +gantt.date.add(right_date, 1, 'day')) {
      gantt.config.end_date = gantt.date.add(gantt.config.end_date, 1, 'day');
      gantt.render();
    }
  }
});

Related sample:  Gantt. Бесконечная прокрутка при перетаскивании временной шкалы

При перетаскивании задачи

Если диапазон дат не задан явно, вызов метода render() при перетаскивании задачи к краям временной шкалы позволяет сохранять видимость диапазона:

gantt.init("gantt_here");
gantt.parse(tasks);
 
gantt.attachEvent("onTaskDrag", function (id, mode, task, original) {
    if (task.start_date <= gantt.getState().min_date ||
        task.end_date >= gantt.getState().max_date) {
        gantt.render()
    }
});

Related sample:  Gantt. Бесконечная прокрутка при перетаскивании задачи (стандартные настройки диапазона)

Если диапазон дат задан явно, его необходимо обновлять при перетаскивании задач к краям:

gantt.init("gantt_here");
gantt.parse(tasks);
 
gantt.config.start_date = new Date(2025, 02, 28)
gantt.config.end_date = new Date(2025, 03, 10)
gantt.render();
 
gantt.attachEvent("onTaskDrag", function (id, mode, task, original) {
    if (+task.start_date <= +gantt.config.start_date) {
        gantt.config.start_date = gantt.date.add(
            gantt.config.start_date, -1, gantt.config.duration_unit
        );
        gantt.render()
    }
    if (+task.end_date >= +gantt.config.end_date) {
        gantt.config.end_date = gantt.date.add(
            gantt.config.end_date, 1, gantt.config.duration_unit
        );
        gantt.render()
    }
});

Related sample:  Gantt. Бесконечная прокрутка при перетаскивании задачи (явные настройки диапазона)

Как загружать задачи динамически

Определяя момент, когда прокрутка достигает последней видимой задачи с помощью события onGanttScroll, можно динамически загружать дополнительные задачи методом parse():

gantt.attachEvent("onGanttScroll", function (left, top) {
    const visibleTasks = gantt.getVisibleTaskCount();
    const lastVisibleTask = gantt.getTaskByIndex(visibleTasks - 1)
 
    if (gantt.getTaskRowNode(lastVisibleTask.id)) {
        const tasks = load_tasks()
        gantt.parse(tasks);
    }
});

Related sample:  Gantt. Динамическая загрузка данных

Как развернуть/свернуть все задачи по кнопке

Методы open() и close() позволяют разворачивать или сворачивать отдельные задачи. Для применения ко всем задачам можно использовать их совместно с функцией eachTask(). Обертывание операции в batchUpdate() гарантирует, что диаграмма будет перерисована только один раз:

function collapseAll() {
    gantt.batchUpdate(function () {
        gantt.eachTask(function (task) {
            gantt.close(task.id)
        })
    })
}
 
function expandAll() {
    gantt.batchUpdate(function () {
        gantt.eachTask(function (task) {
            gantt.open(task.id)
        })
    })
}

Related sample:  Gantt. Кнопки свернуть/развернуть в заголовке Gantt

Related sample:  Gantt. Свернуть/развернуть все задачи

Как отобразить несколько строк в ячейке/заголовке грида

Для отображения многострочного текста в заголовках или ячейках грида необходимо применить специальные CSS-стили.

Для заголовка грида:

.gantt_grid_head_text{
    line-height: 12px;
    white-space:normal;
}

Related sample:  Gantt. Многострочный текст в заголовке грида

Для ячеек грида:

.gantt_tree_content, .gantt_task_content{
    line-height: 12px;
    white-space:normal;
    overflow-wrap: break-word; 
}

Related sample:  Gantt. Многострочный текст в ячейках грида и временной шкале

Related sample:  Gantt. Многострочный текст в ячейках столбца грида

Как добавить пользовательский столбец в грид

Чтобы добавить пользовательский столбец, измените параметр gantt.config.columns. Если указать свойство name, Gantt отобразит соответствующее значение свойства задачи. Также можно использовать функцию template(), чтобы возвращать кастомные данные или HTML-элементы.

gantt.config.columns = [
    /*
    другие столбцы
    */
    {
        name: "progress", label: "Progress", width: 50, resize: true, align: "center", 
        template: function (task) {
            return Math.round(task.progress * 100) + "%"
        }
    },
    /*
    другие столбцы
    */
];

Related sample:  Gantt. Пользовательский столбец с шаблоном для прогресса задачи

Related sample:  Gantt. Пользовательский столбец с шаблоном для кнопок действий

Как добавить пользовательскую кнопку добавления (+)

Создать пользовательскую кнопку добавления можно через определение пользовательского столбца в параметре gantt.config.columns. Имя столбца не должно быть add, чтобы не активировать стандартный столбец добавления. С помощью функции template можно вернуть любой HTML-контент, например, кнопку, и назначить обработчик события для добавления задачи.

gantt.config.columns = [
    /*
    другие столбцы
    */
    {
        name: "add_tasks", label: "+", width: 50, resize: true, align: "center", 
        template: function (task) {
            return `<button onclick='addTask(${task.id})';>`
        }
    },
];

Related sample:  Gantt. Пользовательские столбцы с шаблонами для кнопок добавления (+)

Как добавить пользовательскую шкалу

Чтобы добавить пользовательскую шкалу, начните с создания пользовательской единицы шкалы времени и реализуйте логику вычисления дат.

Пример пользовательской шкалы, отображающей рабочие смены (06:30, 18:30):

gantt.date.custom_scale_start = function (date) {
    return date;
};
 
gantt.date.add_custom_scale = function (date, inc) {
    let next = new Date(date)
    if (!next.getMinutes()) {
        gantt.date.day_start(next)
        next = gantt.date.add(next, 6, "hour");
        next = gantt.date.add(next, 30, "minute");
    }
    else {
        next = gantt.date.add(next, 12 * inc, "hour");
    }
    return next
};
 
gantt.config.scales = [
    { unit: "day", step: 1, date: "%d" },
    { unit: "custom_scale", step: 1, date: "%H:%i" },
];

Related sample:  Gantt. Custom work shift hours on the scale

Еще один пример — пользовательская шкала, где вместо дней используются числа:

gantt.config.scales = [
    {
        unit: "day", step: 1, format: function (date) {
            return gantt.getScale().trace_indexes[+date] + 1
        }
    }
]

Related sample:  Gantt. Numbers of days on the scale

Пример пользовательской шкалы для 5-дневных рабочих недель:

const weekScaleTemplate = function (date) {
    const dateToStr = gantt.date.date_to_str("%d");
    const endDate = gantt.date.add(gantt.date.add(date, 5, "day"), -1, "day");
    return dateToStr(date) + " - " + dateToStr(endDate);
};
 
gantt.date.five_days_start = function (date) {
    return date;
};
 
gantt.date.add_five_days = function (date, inc) {
    if (date.getDay() == 0 || date.getDay() == 6) {
        return gantt.date.add(date, 1 * inc, "day");
    }
    gantt.date.week_start(date);
    return gantt.date.add(date, 5 * inc, "day");
};
 
 
gantt.config.scales = [
    { unit: "month", step: 1, format: "%F, %Y" },
    { unit: "five_days", step: 1, format: weekScaleTemplate },
];
 
gantt.ignore_time = function (date) {
    return date.getDay() == 0 || date.getDay() == 6;
};

Related sample:  5-day work weeks on the scale

Пример пользовательской шкалы, отображающей недели года, где номер недели начинается с первого дня года:

gantt.date.custom_week_start = function (date) {
    return date;
};
 
gantt.date.add_custom_week = function (date, inc) {
    const year_start = new Date(date);
    gantt.date.year_start(year_start);
    const week_number = Math.round(gantt.calculateDuration(year_start, date) / 7);
 
    const next_week = gantt.date.add(year_start, week_number + 1, "week");
    if (next_week.getYear() != date.getYear()) {
        gantt.date.year_start(next_week)
    }
    return next_week;
};
 
 
const custom_week_template = function (date) {
    const year_start = gantt.date.year_start(new Date(date));
    const week_number = Math.round(gantt.calculateDuration(year_start, date) / 7) + 1;
 
    return "Week:" + week_number
}
 
gantt.config.scales = [
    { unit: 'custom_week', step: 1, template: custom_week_template },
    { unit: 'day', step: 1, format: "%d, %M" },
];

Related sample:  Gantt. Weeks of the year on the scale

Как копировать и вставлять задачи

Метод copy() можно использовать для создания глубокой копии объекта задачи. После копирования вы можете назначить склонированной задаче новый ID и добавить ее с помощью методов addTask() или createTask().

Ниже приведен пример добавления кнопки для клонирования задачи:

function clone_task(id) {
    const task = gantt.getTask(id);
    const clone = gantt.copy(task);
    clone.id = +(new Date());
    gantt.addTask(clone, clone.parent, clone.$index)
}
 
gantt.config.columns = [
    /*
    other columns
    */
    {
        name: "clone", label: "clone", width: 44, template: function (task) {
            return "<input type=button value='V' onclick=clone_task(" + task.id + ")>"
        }
    }
];

Related sample:  Gantt. Clone a task

Следующий пример иллюстрирует клонирование задачи вместе со всеми ее подзадачами и связями:

let child_links;
let clone_original_ids_table;
 
function obtain_link_ids(id) {
  const task = gantt.getTask(id);
  const source_links = task.$source;
  for (let i = 0; i < source_links.length; i++) {
    child_links.push(source_links[i]);
  }
}
 
function create_clone_original_ids_table(original_id, clone_id) {
  clone_original_ids_table[original_id] = clone_id;
}
 
function clone_child_links() {
 for (let i = 0; i < child_links.length; i++) {
  const link = gantt.getLink(child_links[i]);
  if (clone_original_ids_table[link.source] && clone_original_ids_table[link.target]){
    const clone_link = {};
    clone_link.id = gantt.uid();
    clone_link.target = clone_original_ids_table[link.target];
    clone_link.source = clone_original_ids_table[link.source];
    clone_link.type = link.type;
    gantt.addLink(clone_link)
  }
 }
}
 
function clone_children(id, new_parent) {
  const children = gantt.getChildren(id)
  for (let i = 0; i < children.length; i++) {
    const child_original = gantt.getTask(children[i]);
    const child_clone = gantt.copy(child_original);
    child_clone.id = gantt.uid();
    child_clone.parent = new_parent;
    gantt.addTask(child_clone, child_clone.parent, child_clone.$index);
 
    obtain_link_ids(child_original.id);
    create_clone_original_ids_table(child_original.id, child_clone.id);
 
    if (gantt.hasChild(child_original.id)) clone_children(
      child_original.id, child_clone.id
    );
  }
}
 
function clone_task(id) {
  const task = gantt.getTask(id);
  const clone = gantt.copy(task);
  clone.id = gantt.uid();
  gantt.addTask(clone, clone.parent, clone.$index);
 
  child_links = [];
  obtain_link_ids(id);
 
  clone_original_ids_table = {};
  create_clone_original_ids_table(task.id, clone.id);
 
  if (gantt.hasChild(id)) {
    clone_children(id, clone.id)
  }
 
  clone_child_links()
}
 
gantt.config.order_branch = true;
gantt.config.order_branch_free = true;
 
gantt.config.columns = [
  /*
  other columns
  */
  {
    name: "clone", label: "clone", width: 44, template: function (task) {
      return "<input type=button value='V' onclick=clone_task(" + task.id + ")>"
    }
  }
];

Related sample:  Gantt. Clone a task with all its subtasks and links

Еще один пример показывает, как реализовать копирование через горячие клавиши: выберите задачи, нажмите Ctrl + C для копирования и Ctrl + V для вставки в качестве подзадач выбранной задачи:

gantt.plugins({
    keyboard_navigation: true,
    multiselect: true,
})
 
let tasks_to_copy = [];
 
gantt.ext.keyboardNavigation.addShortcut("ctrl+c", function (e) {
    tasks_to_copy = [];
    gantt.eachSelectedTask(function (task_id) {
        tasks_to_copy.push(task_id);
    });
}, "taskRow");
gantt.ext.keyboardNavigation.addShortcut("ctrl+v", function (e) {
    const new_parent = gantt.getSelectedId();
    for (let i = 0; i < tasks_to_copy.length; i++) {
        const task = gantt.copy(gantt.getTask(tasks_to_copy[i]));
        task.id = +new Date() + '+' + Math.floor(Math.random() * 10);
        gantt.addTask(task, new_parent)
    }
    gantt.getTask(new_parent).$open = true;
    gantt.render()
}, "taskRow");

Related sample:  Gantt. Copy and paste tasks via Ctrl+C, Ctrl+V

Как добавить диаграмму ресурсов или пользовательские стили в экспортируемый PDF-файл

Чтобы добавить пользовательские стили или диаграммы ресурсов в экспортируемый PDF, экспортируйте данные в raw режиме и добавьте стили через параметры header или footer функции экспорта.

Например, вы можете сохранить стили в переменной и включить эту переменную в параметр header:

const header = `
    .gantt_bar_task {
        background: orange;
    }
 
    .gantt_task_progress {
        background-color: rgba(33, 33, 33, 0.17);
    }
`
 
gantt.exportToPDF({
       header: "<style>" + header + "</style>"
});

Related sample:  Gantt. Export Gantt to PDF (styles from a variable)

Или получить содержимое элемента <style> на странице и добавить его так:

gantt.exportToPDF({
    raw: true,
    header: "<style>" + document.getElementById("styles").innerHTML + "</style>"
});
 
<style id='styles'>
    .gantt_bar_task {
        background: orange;
    }
 
    .gantt_task_progress {
        background-color: rgba(33, 33, 33, 0.17);
    }
</style>

Related sample:  Gantt. Export Gantt to PDF (styles from <style> element)

Related sample:  Gantt. Export Gantt with custom icons to PDF

Пример экспорта диаграммы Gantt с легендой:

Related sample:  Gantt. Export Gantt with legend to PDF

Примеры экспорта диаграммы загрузки ресурсов и гистограммы:

Related sample:  Gantt. Export Gantt with resource load diagram to PDF

Related sample:  Gantt. Export Gantt with resource histogram to PDF

Как рассчитать прогресс задачи в зависимости от дочерних задач

Один из простых способов — обновлять прогресс родительской задачи сразу после изменения дочерней задачи. Для обхода родительских задач удобно использовать метод eachParent().

В примере ниже прогресс родительских задач рассчитывается только на основе прогресса их дочерних задач:

gantt.config.auto_types = true;
 
gantt.templates.progress_text = function (start, end, task) {
    return "<span style='text-align:left;'>" + Math.round(task.progress * 100) 
          + "% </span>";
};
 
gantt.init("gantt_here");
gantt.parse({
    "data": [
        ...
    ]
});
 
gantt.attachEvent("onAfterTaskUpdate", function (id, task) {
    parentProgress(id)
});
gantt.attachEvent("onTaskDrag", function (id, mode, task, original) {
    if (mode == "progress") {
        parentProgress(id)
    }
});
gantt.attachEvent("onAfterTaskAdd", function (id) {
    parentProgress(id)
});
gantt.attachEvent("onAfterTaskDelete", function (id, task) {
    if (task.parent) {
        const siblings = gantt.getChildren(task.parent);
        if (siblings.length) {
            parentProgress(siblings[0])
        }
    }
});
 
function parentProgress(id) {
    gantt.eachParent(function (task) {
        const children = gantt.getChildren(task.id);
        let childProgress = 0;
        for (let i = 0; i < children.length; i++) {
            const child = gantt.getTask(children[i])
            childProgress += (child.progress * 100);
        }
        task.progress = childProgress / children.length / 100;
    }, id)
    gantt.render();
}

Related sample:  Gantt. Calculate progress of a parent task dynamically

В следующем примере прогресс родительских задач рассчитывается с учетом как прогресса, так и длительности дочерних задач:

function calculateSummaryProgress(task) {
    if (task.type != gantt.config.types.project)
        return task.progress;
    var totalToDo = 0;
    var totalDone = 0;
    gantt.eachTask(function (child) {
        if (child.type != gantt.config.types.project) {
            totalToDo += child.duration;
            totalDone += (child.progress || 0) * child.duration;
        }
    }, task.id);
    if (!totalToDo) return 0;
    else return totalDone / totalToDo;
}
 
function refreshSummaryProgress(id, submit) {
    if (!gantt.isTaskExists(id))
        return;
 
    var task = gantt.getTask(id);
    var newProgress = calculateSummaryProgress(task);
 
    if (newProgress !== task.progress) {
        task.progress = newProgress;
 
        if (!submit) {
            gantt.refreshTask(id);
        } else {
            gantt.updateTask(id);
        }
    }
 
    if (!submit && gantt.getParent(id) !== gantt.config.root_id) {
        refreshSummaryProgress(gantt.getParent(id), submit);
    }
}
 
 
gantt.attachEvent("onParse", function () {
    gantt.eachTask(function (task) {
        task.progress = calculateSummaryProgress(task);
    });
});
 
gantt.attachEvent("onAfterTaskUpdate", function (id) {
    refreshSummaryProgress(gantt.getParent(id), true);
});
 
gantt.attachEvent("onTaskDrag", function (id) {
    refreshSummaryProgress(gantt.getParent(id), false);
});
gantt.attachEvent("onAfterTaskAdd", function (id) {
    refreshSummaryProgress(gantt.getParent(id), true);
});
 
 
(function () {
    var idParentBeforeDeleteTask = 0;
    gantt.attachEvent("onBeforeTaskDelete", function (id) {
        idParentBeforeDeleteTask = gantt.getParent(id);
    });
    gantt.attachEvent("onAfterTaskDelete", function () {
        refreshSummaryProgress(idParentBeforeDeleteTask, true);
    });
})();
 
...
 
gantt.config.auto_types = true;
 
gantt.templates.progress_text = function (start, end, task) {
    return "<span style='text-align:left;'>" + Math.round(task.progress * 100) 
          + "% </span>";
};
 
gantt.templates.task_class = function (start, end, task) {
    if (task.type == gantt.config.types.project)
        return "hide_project_progress_drag";
};

Related sample:  Calculate Progress of Summary Tasks

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

Метод addTaskLayer() позволяет добавлять пользовательские HTML-элементы на временную шкалу и поддерживает их вертикальное и горизонтальное перемещение.

В примере ниже показано, как можно менять порядок задач по вертикали на временной шкале, аналогично тому, как задачи можно менять местами в гриде:

Related sample:  Gantt. Reorder tasks vertically in timeline

В другом примере показано, как менять порядок разделённых задач и размещать задачи на одной строке:

Related sample:  Gantt. Reorder split tasks vertically in timeline

Как зафиксировать столбцы в гриде

Такого эффекта можно добиться с помощью CSS. Столбец, который нужно зафиксировать, должен иметь позицию 'relative'. Свойство 'left' должно соответствовать текущей позиции скроллбара. Для актуализации значения можно добавить обработчик события на скроллбар и изменять CSS-переменную:

gantt.attachEvent("onGanttReady", function () {
    const el = document.querySelector(".gantt_hor_scroll");
    if (el) {
        el.addEventListener('scroll', function () {
            document.documentElement.style.setProperty(
              '--gantt-frozen-column-scroll-left', el.scrollLeft + "px"
            );
        });
    }
});
 
const textEditor = { type: "text", map_to: "text" };
const start_dateEditor = { type: "date", map_to: "start_date" };
const end_dateEditor = { type: "date", map_to: "end_date" };
const durationEditor = { type: "number", map_to: "duration", min: 0, max: 100 };
 
 
gantt.config.columns = [
    { name: "text", tree: true, width: 150, resize: true, editor: textEditor },
    { name: "start_date", align: "center", width: 120, resize: true, 
      editor: start_dateEditor },
    { name: "end_date", label: "End Time", align: "center", width: 120, 
      resize: true, editor: end_dateEditor },
    { name: "duration", align: "center", width: 80, resize: true, 
      editor: durationEditor },
    { name: "progress", label: "Progress", width: 80, align: "center", 
      resize: true },
    {
        name: "custom", label: "Custom", width: 180, align: "center", 
        resize: true, template: function (task) {
            return Math.round(Math.random() * 100)
        }
    },
    { name: "add", width: 44 }
];
 
gantt.config.layout = {
    css: "gantt_container",
    cols: [
        {
            rows: [
                {
                    view: "grid", scrollable: true, 
                    scrollX: "scrollHor1", scrollY: "scrollVer"
                },
                {
                    view: "scrollbar", id: "scrollHor1",
                    scroll: 'x', group: 'hor'
                },
            ]
        },
        { resizer: true, width: 1 },
        {
            rows: [
                {
                    view: "timeline", scrollX: "scrollHor", scrollY: "scrollVer"
                },
                {
                    view: "scrollbar", id: "scrollHor",
                    scroll: 'x', group: 'hor'
                },
            ]
        },
        { view: "scrollbar", id: "scrollVer" }
    ]
}

Дополнительно добавьте следующие CSS-стили:

:root {
    --gantt-frozen-column-scroll-left: 0px;
}
 
.gantt_cell:nth-child(1),
.gantt_grid_head_cell:nth-child(1) {
    background: Azure;
    position: relative;
    left: var(--gantt-frozen-column-scroll-left);
}
 
.gantt_grid_editor_placeholder[data-column-name="text"] {
    left: var(--gantt-frozen-column-scroll-left) !important;
}
 
.gantt_grid_head_cell:nth-child(1) {
    z-index: 1;
}

Related sample:  Gantt. Frozen column in Grid (via CSS)

В качестве альтернативы можно настроить несколько представлений грида, однако такой подход не очень хорошо работает с встроенными редакторами:

gantt.config.columns = [
    { name: "start_date", align: "center", width: 80, resize: true },
    { name: "end_date", label: "End Date", align: "center", width: 80, resize: true },
    { name: "duration", width: 60, align: "center", resize: true },
    { name: "progress", label: "Progress", width: 60, align: "center", resize: true },
    { name: "add", width: 44 }
];
 
 
const fixedColumn = {
    columns: [
        { name: "text", tree: true, width: 200, resize: true },
    ]
};
 
gantt.config.layout = {
  css: "gantt_container",
  cols: [
    {
      width: 400,
      //min_width: 100,
      rows: [
        {
          group: "gantt",
          cols: [
            {
              rows: [
                { view: 'grid', config: fixedColumn, bind: "task", 
                  scrollY: 'gridScrollY' }
              ]
            },
            {
              rows: [
                { view: 'grid', bind: "task", scrollX: 'gridScrollX', 
                  scrollable: true, scrollY: 'gridScrollY' },
                { view: 'scrollbar', id: 'gridScrollX' }
              ]
            },
            { view: 'scrollbar', id: 'gridScrollY' }
          ]
        }
      ]
    },
    { resizer: true, width: 1 },
    {
      rows: [
        {
          group: "gantt",
          cols: [
            {
              rows: [
                { view: "timeline", scrollX: "scrollHor", scrollY: "scrollVer" },
                { view: "scrollbar", id: "scrollHor" }
              ]
            },
            { view: 'scrollbar', id: 'scrollVer' }
          ]
        }
      ]
    }
  ]
}

Related sample:  Gantt. Fixed column in Grid (several grid views)

Как добавить легенду в Gantt

В Gantt нет встроенной функции для добавления легенды. Ближайший вариант — Overlay extension, но это не совсем то же самое и возможности настройки ограничены.

Тем не менее, добавить легенду достаточно просто. Можно создать элемент легенды в HTML и вставить его в контейнер Gantt следующим образом:

gantt.$root.appendChild(legend);

Ниже приведён живой пример, где легенда появляется после нажатия кнопки "Toggle legend" над Gantt:

Related sample:  Gantt. Add information legend

Чтобы добавить интерактивность, можно навешивать обработчики событий прямо на элемент легенды или обрабатывать события на корневом элементе Gantt с помощью делегирования:

gantt.event(gantt.$root, "click", function(e){
    var closest = gantt.utils.dom.closest;
    if(closest(e.target, ".gantt-legend")) {
        gantt.message("Mouse click inside the legend element");
    }
});
К началу