Guía R-009 – Gráficos de Gantt

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.

Adjuntos

Archivo Tamaño de archivo Descargas
zip Proyecto React de "gantt-react" 177 KB 6

Blog personal para facilitar soporte gratuito a usuarios de React y PHPRunner