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.