El gráfico de Gantt utilizado en mi ejemplo de Gestión de Proyectos, es muy potente, pero gráficamente se le ve muy antiguo.
En esta nueva plataforma de desarrollo he estado revisando y probando aquellas soluciones de gráficos de Gantt que son FREE y después de mucho probar he seleccionado «gantt-task-react«, en su versión 0.3.9.
Objetivo
Seleccionar un componente free, para hacer gráficos de Gantt que tenga las siguientes características:
- Que se pueda adaptar a diferentes idiomas.
- Que se pueda establecer en cada tareas (color, tipo, fecha de inicio, fecha final, dependencia, agrupación y recursos asignados).
- Que se pueda colapsar y extender las tareas de grupo.
- Que disponga de tooltip para mostrar información adicional de la tarea
- Que se pueda interactuar con el gráfico, por ejemplo, para consulta o actualización de la tarea.
DEMO: https://fhumanes.com/gantt-react/
Solución Técnica
Como os he indicado, he seleccionado gantt-task-react y para realizar el ejemplo me he apoyado en las IA de ChatGPT y DeepSeek, aunque ninguno de los 2 me dio un ejemplo completo y funcionando, por lo que entre lo que me facilitaba una y la otra, he conseguido un ejemplo del que estoy satisfecho. Cómo he indicado en otras ocasiones, este entorno de React es muy conocido por las IA y son capaces de facilitarte código completo si le describes con cierto detalle lo que necesitas. Es una gran ayuda para aquellos que no tenemos un compañero/a con quién compartir inquietudes y dudas.
Paso a facilitaros los 2 ficheros importantes del ejemplo.
App.js
import React, { useState } from 'react';
import { Gantt } from 'gantt-task-react';
import "gantt-task-react/dist/index.css";
import './App.css';
// Recursos del proyecto
const recursos = [
{ id: "r1", nombre: "Juan Pérez" },
{ id: "r2", nombre: "María García" },
{ id: "r3", nombre: "Carlos López" },
{ id: "r4", nombre: "Ana Martínez" },
];
// Componente para el tooltip
const CustomTooltip = ({ task }) => {
if (!task) return null;
const formatFecha = (date) => date?.toLocaleDateString('es-ES') || 'No definida';
const formatRecursos = (ids) => {
if (!ids || !Array.isArray(ids)) return "Sin asignar";
return ids.map(id => recursos.find(r => r.id === id)?.nombre).filter(Boolean).join(", ") || "Sin asignar";
};
return (
<div className="custom-tooltip">
<div><strong>{task.name}</strong></div>
<div>Recursos: {formatRecursos(task.resources)}</div>
<div>Fecha niciall: {formatFecha(task.start)}</div>
<div>Fecha final: {formatFecha(task.end)}</div>
<div>Duración: {task.duration || 0} días</div>
<div>Avance: {task.progress || 0}%</div>
{task.dependencies && (
<div>Depende de: {task.dependencies.join(', ')}</div>
)}
</div>
);
};
// Componente principal
const GanttEjemplo = () => {
const [view, setView] = useState('Day');
const [showTaskList, setShowTaskList] = useState(true);
const [tasks, setTasks] = useState([
{
id: 'Proyecto',
name: 'Proyecto de Desarrollo',
start: new Date(2025, 4, 1),
end: new Date(2025, 4, 25),
progress: 45,
type: 'project',
hideChildren: false,
styles: { progressColor: '#ff9e0d' },
duration: 14,
},
{
id: 'Analisis',
name: 'Análisis de Requisitos',
start: new Date(2025, 4, 1),
end: new Date(2025, 4, 5),
progress: 80,
type: 'task',
project: 'Proyecto',
resources: [recursos[0].id, recursos[1].id],
styles: { progressColor: '#4CAF50' },
duration: 4,
},
{
id: 'Diseno',
name: 'Diseño de Arquitectura',
start: new Date(2025, 4, 6),
end: new Date(2025, 4, 10),
progress: 30,
type: 'task',
project: 'Proyecto',
dependencies: ['Analisis'], // Depende de Análisis
resources: [recursos[2].id],
styles: { progressColor: '#2196F3' },
duration: 4,
},
{
id: 'Implementacion',
name: 'Implementación',
start: new Date(2025, 4, 11),
end: new Date(2025, 4, 20),
progress: 10,
type: 'task',
project: 'Proyecto',
dependencies: ['Diseno'], // Depende de Diseño
resources: [recursos[1].id, recursos[3].id],
styles: { progressColor: '#9C27B0' },
duration: 9,
},
{
id: 'Pruebas',
name: 'Pruebas',
start: new Date(2025, 4, 21),
end: new Date(2025, 4, 25),
progress: 0,
type: 'task',
project: 'Proyecto',
dependencies: ['Implementacion'], // Depende de Implementación
resources: [recursos[0].id, recursos[2].id],
styles: { progressColor: '#F44336' },
duration: 4,
},
{
id: 'Cierre',
name: 'Cierre del Proyecto',
start: new Date(2025, 4, 30),
end: new Date(2025, 4, 30),
progress: 25,
type: 'milestone',
project: 'Proyecto',
resources: [recursos[3].id],
styles: { progressColor: '#607D8B' },
duration: 29,
},
]);
// Colapsar/expandir todos los proyectos
const toggleAllProjects = (collapse) => {
setTasks(tasks.map(task =>
task.type === 'project'
? { ...task, hideChildren: collapse }
: task
));
};
// Manejo de colapsar/expandir un proyecto
const handleExpanderClick = (task) => {
setTasks(tasks.map((t) => (t.id === task.id ? task : t)));
console.log("On expander click Id:" + task.id);
};
// Componente personalizado para la cabecera de la lista de tareas
const CustomTaskListHeader = () => {
return (
<div className="gantt-task-list-header" style={{ display: 'flex' }}>
<div style={{ width: '100%', fontWeight: 'normal' }}>Nombre</div>
{/*
<div style={{ width: '33%', fontWeight: 'normal' }}>Inicio</div>
<div style={{ width: '33%', fontWeight: 'normal' }}>Fin</div>
*/ }
</div>
);
};
// Componente de tabla de tareas personalizado
const TaskListTable = ({ tasks }) => (
<>
{tasks.map((task) => (
<div className="gantt-task-list-item" key={task.id} style={{ display: "flex", justifyContent: "flex-start", padding: "10px" }}>
<div style={{ width: "100%", height: "22px", textWrap: "nowrap" ,padding: "8px 0px 0px 0px" }}>{task.name}</div>
</div>
))}
</>
);
return (
<div className="gantt-container">
<div className="gantt-controls">
<button onClick={() => setView('Day')}>Día</button>
<button onClick={() => setView('Week')}>Semana</button>
<button onClick={() => setView('Month')}>Mes</button>
<button onClick={() => setView('Year')}>Año</button>
<label>
<input
type="checkbox"
checked={showTaskList}
onChange={() => setShowTaskList(!showTaskList)}
/>
Mostrar nombres de tareas
</label>
<button onClick={() => toggleAllProjects(true)}>Colapsar Todos</button>
<button onClick={() => toggleAllProjects(false)}>Expandir Todos</button>
</div>
<h2>Planificación del Proyecto</h2>
<div className="gantt-chart-container">
<Gantt
tasks={tasks}
onExpanderClick={handleExpanderClick}
viewMode={view}
locale="spa"
listCellWidth={showTaskList ? true : false} // Si fijo poner "250px"
TooltipContent={CustomTooltip}
TaskListHeader={CustomTaskListHeader}
TaskListTable={TaskListTable}
columnWidth={70}
todayColor="rgba(47, 0, 255, 0.1)"
/>
</div>
<div className="leyenda-recursos">
<h3>Recursos asignados:</h3>
<ul>
{recursos.map((recurso, index) => (
<li key={recurso.id}>
<span className="color-recurso" style={{
backgroundColor: `hsl(${index * 90}, 70%, 50%)`
}}></span>
{recurso.nombre}
</li>
))}
</ul>
</div>
</div>
);
};
export default GanttEjemplo;
App.css
.gantt-container {
font-family: Arial, sans-serif;
margin: 20px;
}
.gantt-controls {
margin-bottom: 20px;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.gantt-controls button {
padding: 5px 10px;
background: #f0f0f0;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
.gantt-controls button:hover {
background: #e0e0e0;
}
.gantt-chart-container {
border: 1px solid #eee;
border-radius: 5px;
overflow: hidden;
margin-top: 15px;
}
.gantt-task-list-item {
/* padding: 8px 10px; */
/* border-bottom: 1px solid #eee; */
font-size: 14px;
background-color: #fff;
}
.gantt-task-list-item:nth-child(even) {
background-color: #f5f5f5;
}
.custom-tooltip {
background: white;
padding: 10px;
border-radius: 3px;
box-shadow: 0 0 5px rgba(0,0,0,0.1);
border: 1px solid #ddd;
font-size: 14px;
min-width: 200px;
}
.leyenda-recursos {
margin-top: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 5px;
}
.leyenda-recursos ul {
list-style: none;
padding: 0;
margin: 0;
}
.leyenda-recursos li {
display: flex;
align-items: center;
margin: 5px 0;
padding: 3px 0;
}
.color-recurso {
display: inline-block;
width: 15px;
height: 15px;
margin-right: 10px;
border-radius: 3px;
border: 1px solid #ddd;
}
.gantt-table-expander {
cursor: pointer;
user-select: none;
}
.gantt-task-list-header {
padding: 18px 0px 0px 0px;
/* background-color: #f5f5f5; */
border-bottom: 1px solid #ddd;
font-weight: bold;
width: 200px;
height: 32px;
text-anchor: middle;
text-align: center;
}
.gantt-task-list-header div {
padding: 0 5px;
}
También os dejo el proyecto para que lo podáis descargar e instalar en vuestros PC’s.
