Gantt Chart

Hello,
a must have component is a robust Gantt Chart that would have mulitple views including hour, days, month, and year. such a component would be of great assistance.

Hey @Fady1956,

We don’t have plans for such component however we accept pull requests!

I wish I had the knowledge to create such a component.
It would have completed your component list beautifully.
Thanks and Best regards

I know it is not ideal, but I finally created a GanttChart in javascript and I was able to incorporate it in a radzen blazor app.
this is the code:
class GanttChart {
constructor(containerId, options) {
this.container = document.getElementById(containerId);
if (!this.container) {
console.error(Container with id ${containerId} not found.);
return;
}
this.dataUrl = options.dataUrl;
this.headers = options.headers || ['Priority Area', 'Main Topic', 'Expert', 'Days Allocated', 'Start', 'Deadline', '% Completed', 'Status'];
this.columnWidths = options.columnWidths;
this.groupBy = options.groupBy || null;
this.startDate = new Date(options.startDate) || new Date('2024-01-01');
this.endDate = new Date(options.endDate) || new Date('2024-07-30');

    this.init();
}

formatDate(date) {
    let day = date.getDate();
    let month = date.getMonth() + 1;
    let year = date.getFullYear() % 100;
    day = day < 10 ? '0' + day : day;
    month = month < 10 ? '0' + month : month;
    year = year < 10 ? '0' + year : year;
    return `${day}-${month}-${year}`;
};

init() {
    this.fetchData().then(tasks => {
        this.tasks = tasks;
        this.filteredTasks = tasks; // Add this line to keep track of filtered tasks
        this.createGanttChart();
    }).catch(error => {
        console.error('Failed to load data:', error);
    });
}

async fetchData() {
    const response = await fetch(this.dataUrl);
    if (!response.ok) {
        throw new Error(`HTTP error! Status: ${response.status}`);
    }
    try {
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('Failed to parse JSON:', error);
        console.log('Raw response:', await response.text()); // This line helps to understand what the server actually returned
        throw new Error('Failed to parse JSON.');
    }
}

filterTasks(event) {
    const query = event.target.value.toLowerCase();
    this.filteredTasks = this.tasks.filter(task =>
        Object.values(task).some(value =>
            String(value).toLowerCase().includes(query)
        )
    );
    this.createGanttChart();
}

createGanttChart() {
    // Clear previous contents
    const header = document.getElementById('gantt-header');
    const timelineHeader = document.getElementById('gantt-timeline-header');
    const body = document.getElementById('gantt-body');
    const timelineBody = document.getElementById('gantt-timeline-body');

    header.innerHTML = '';
    timelineHeader.innerHTML = '';
    body.innerHTML = '';
    timelineBody.innerHTML = '';

    // Create search input
    if (!document.querySelector('.search-input')) {
        const searchInput = document.createElement('input');
        searchInput.type = 'text';
        searchInput.placeholder = 'Search...';
        searchInput.className = 'search-input  ms-5';
        searchInput.addEventListener('input', this.filterTasks.bind(this));

        // Create buttons for expand and collapse
        const buttonContainer = document.createElement('div');
        buttonContainer.className = 'button-container';

        const expandAllButton = document.createElement('button');
        expandAllButton.textContent = 'Expand All';
        expandAllButton.addEventListener('click', () => {
            document.querySelectorAll('.task-row').forEach(row => row.style.display = 'table-row');
            document.querySelectorAll('.timeline-row').forEach(row => row.style.display = 'table-row');
            document.querySelectorAll('.collapse-icon').forEach(icon => icon.textContent = 'β–Ό');
        });
        expandAllButton.className = 'btn btn-outline-secondary  ms-5';
        const collapseAllButton = document.createElement('button');
        collapseAllButton.textContent = 'Collapse All';
        collapseAllButton.addEventListener('click', () => {
            document.querySelectorAll('.task-row').forEach(row => row.style.display = 'none');
            document.querySelectorAll('.timeline-row').forEach(row => row.style.display = 'none');
            document.querySelectorAll('.collapse-icon').forEach(icon => icon.textContent = 'β–Ί');
        });
        collapseAllButton.className = 'btn btn-outline-warning  ms-5';
        buttonContainer.appendChild(searchInput);
        buttonContainer.appendChild(expandAllButton);
        buttonContainer.appendChild(collapseAllButton);
        const parentContainer = this.container.parentElement;
        parentContainer.insertBefore(buttonContainer, this.container);
    }

    // Create header
    const headerRow = document.createElement('tr');
    this.headers.forEach((headerText, index) => {
        const headerCell = document.createElement('th');
        headerCell.textContent = headerText;
        headerCell.style.width = this.columnWidths[index];
        headerRow.appendChild(headerCell);
    });
    header.appendChild(headerRow);

    // Helper functions to calculate date differences and position
    const parseDate = (dateStr) => new Date(dateStr);
    const dateDiffInDays = (a, b) => (parseDate(b) - parseDate(a)) / (1000 * 60 * 60 * 24);

    const totalDays = dateDiffInDays(this.startDate, this.endDate);

    // Create timeline header for days
    const timelineHeaderRow = document.createElement('tr');
    for (let i = 0; i <= totalDays; i++) {
        const dayCell = document.createElement('th');
        dayCell.className = 'day-cell';
        const date = new Date(this.startDate);
        date.setDate(this.startDate.getDate() + i);
        dayCell.textContent = this.formatDate(date);
        timelineHeaderRow.appendChild(dayCell);
    }
    timelineHeader.appendChild(timelineHeaderRow);

    // Function to format date as dd-mm-yy
    const calculatePosition = (start, allocated) => ({
        left: dateDiffInDays(this.startDate, start) * 24,
        width: allocated * 24
    });

    // Group tasks by company
    const tasksByCompany = this.filteredTasks.reduce((acc, task) => {
        acc[task.company] = acc[task.company] || [];
        acc[task.company].push(task);
        return acc;
    }, {});

    // Create rows for each company and their tasks
    Object.keys(tasksByCompany).forEach(company => {
        // Company row
        const companyRow = document.createElement('tr');
        companyRow.className = 'company-row';
        const companyCell = document.createElement('td');
        companyCell.textContent = company;
        companyCell.colSpan = this.headers.length;

        const collapseIcon = document.createElement('span');
        collapseIcon.className = 'collapse-icon';
        collapseIcon.textContent = 'β–Ό';
        companyCell.insertBefore(collapseIcon, companyCell.firstChild);

        companyRow.appendChild(companyCell);
        body.appendChild(companyRow);

        const timelineCompanyRow = document.createElement('tr');
        timelineCompanyRow.className = 'company-row';
        const timelineCompanyCell = document.createElement('td');
        timelineCompanyCell.colSpan = totalDays + 1;
        timelineCompanyRow.appendChild(timelineCompanyCell);
        timelineBody.appendChild(timelineCompanyRow);

        // Container for task rows to facilitate collapsing
        const taskRows = [];
        const timelineTaskRows = [];

        // Task rows
        tasksByCompany[company].forEach(task => {
            // Task detail row
            const taskRow = document.createElement('tr');
            taskRow.className = 'task-row';

            const taskRowData = [
                task.priorityArea,
                task.mainTopic,
                task.expert,
                task.allocated,
                task.start,
                task.deadline,
                task.percentCompleted * 100,
                task.status
            ];

            taskRowData.forEach((data, index) => {
                const cell = document.createElement('td');
                cell.textContent = data;
                cell.style.width = this.columnWidths[index];
                taskRow.appendChild(cell);
            });

            taskRows.push(taskRow);

            // Create timeline task row
            const timelineRow = document.createElement('tr');
            timelineRow.className = 'timeline-row';

            // Create the task bar
            const position = calculatePosition(task.start, task.allocated);
            const taskBar = document.createElement('div');
            taskBar.setAttribute('data-bs-toggle', 'popover');
            taskBar.setAttribute('data-bs-trigger', 'hover focus');
            taskBar.setAttribute('data-bs-custom-class', 'custom-popover');
            taskBar.setAttribute('data-bs-title', task.expert);
            taskBar.setAttribute('data-bs-content', task.mainTopic);
            taskBar.setAttribute('data-bs-placement', 'right');
            taskBar.className = 'gantt-task';
            taskBar.style.left = `${position.left}px`;
            taskBar.style.width = `${position.width}px`;
            taskBar.textContent = `${task.percentCompleted * 100}%`;

            // Create the progress bar inside the task bar
            const progressBar = document.createElement('div');
            progressBar.className = 'gantt-task-progress';
            progressBar.style.width = `${task.percentCompleted * 100}px`;
            taskBar.appendChild(progressBar);

            const timelineCell = document.createElement('td');
            timelineCell.colSpan = totalDays + 1;
            timelineCell.style.position = 'relative';
            timelineCell.style.height = '30px';

            timelineCell.appendChild(taskBar);

            timelineRow.appendChild(timelineCell);

            timelineTaskRows.push(timelineRow);
        });

        // Append task rows to the body
        taskRows.forEach(row => body.appendChild(row));
        timelineTaskRows.forEach(row => timelineBody.appendChild(row));

        // Add collapse functionality
        collapseIcon.addEventListener('click', () => {
            const isVisible = taskRows[0].style.display !== 'none';
            taskRows.forEach(row => row.style.display = isVisible ? 'none' : 'table-row');
            timelineTaskRows.forEach(row => row.style.display = isVisible ? 'none' : 'table-row');
            collapseIcon.textContent = isVisible ? 'β–Ί' : 'β–Ό';
        });
    });

    document.querySelectorAll('[data-bs-toggle="popover"]')
        .forEach(popover => {
            new bootstrap.Popover(popover)
        });

    const ganttTasks = document.querySelector('.gantt-tasks');
    const ganttTimeline = document.querySelector('.gantt-timeline');
    const ganttTimelineHeader = document.querySelector('.gantt-timeline-header');

    if (ganttTasks) {
        ganttTasks.addEventListener('scroll', function () {
            ganttTimeline.scrollTop = event.target.scrollTop;
        });
    }
    if (ganttTimeline) {
        ganttTimeline.addEventListener('scroll', function () {
            ganttTasks.scrollTop = event.target.scrollTop;
            ganttTimelineHeader.scrollLeft = event.target.scrollLeft;
        });
    }
    console.log("Gantt chart created!");
}

}
if that can help someone to create a razen Blazor component from it, it would be great

1 Like

It's a good start, please.. the code is not fully readable, can you post it in a way it works?