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
It's a good start, please.. the code is not fully readable, can you post it in a way it works?