Cómo funciona
La ruta crítica sobre tiempo laborable
Dibujar barras en una línea de tiempo es fácil. Lo que hace a MS Project ser MS Project es que las fechas no se escriben: se calculan a partir de duraciones, vínculos, restricciones y un calendario. Esta página explica ese cálculo y cómo se pinta. Las demos usan el código real de la app, cada una con su propio editor (no tocan tu proyecto).
1. Barras o programación
Un componente de Gantt «básico» guarda en cada tarea su comienzo y su fin, y deja que el usuario los mueva. Un planificador guarda otra cosa: cuánto dura cada tarea y de qué depende. Las fechas salen de un motor de programación que se vuelve a ejecutar con cada cambio. Ese motor (más los calendarios y la línea base) es justo lo que las librerías gratuitas dejan para su edición de pago.
Aquí el motor es una función pura, schedule(proyecto), sin React ni DOM, probada con redes de libro de texto calculadas a mano. Con 10.000 tareas tarda unos 8 ms, así que se ejecuta entero en cada edición: no hace falta recálculo incremental.
2. Tiempo laborable
«3 días» no son 72 horas: son 3 × 8 horas de trabajo, saltando la comida, la noche y el fin de semana. El motor no hace cuentas con fechas sino con un contador de minutos laborables, que solo avanza en horas de trabajo. Sumar una duración es sumar un número; el calendario traduce al final.
Hay un detalle que confunde a todos los que lo programan por primera vez: el mismo minuto laborable es el fin de un día y el comienzo del siguiente. Mueve el contador hasta 2.400 (5 días):
El mismo instante laborable…
Al terminar justo a las 12:00 o a las 17:00, el fin se queda ahí y el comienzo salta al siguiente minuto de trabajo. Por eso una tarea de 5 días que empieza el lunes termina el viernes a las 17:00, no el lunes siguiente.
Las fechas se guardan como «minutos de pared»: los componentes que se ven en el reloj pasados por Date.UTC. Así ninguna zona horaria ni cambio de horario de verano mueve una tarea.
// cum(d) = minutos laborables desde el día ancla hasta el inicio del día d (tabla a demanda)
toWork(w) = cum(día de w) + minutos trabajados ese día antes de la hora de w
toWall(x, borde):
día = el mayor d con cum(d) <= x (con "finish": cum(d) < x) // búsqueda binaria
y se recorren los intervalos de ese día hasta gastar x - cum(día)3. El grafo y su orden
Cada tarea es un nodo y cada vínculo una arista. Para calcular una tarea hay que haber calculado antes todas sus predecesoras: eso es un orden topológico, que el algoritmo de Kahn obtiene en tiempo lineal quitando nodos sin aristas de entrada. Si al final quedan nodos, hay un ciclo: es lo que MS Project llama «vínculo circular», y aquí se detecta igual, probando el cambio con el propio motor antes de aceptarlo.
Las tareas resumen complican el grafo: un vínculo hacia una resumen empuja a todas sus hijas, y uno desde ella espera a que terminen todas. Si la resumen fuera un solo nodo, habría un ciclo (la resumen depende de sus hijas y sus hijas de ella). La solución es partirla en dos nodos:
predecesoras ──▶ [entrada de la resumen] ──▶ hija 1 ──▶ [salida de la resumen] ──▶ sucesoras
└──▶ hija 2 ──┘
// entrada: el límite que llega de fuera; salida: min(comienzos) y max(fines) de las hijas4. Las dos pasadas
Hacia adelante, en orden topológico: cada tarea empieza lo antes que le permiten sus predecesoras (comienzo y fin tempranos, ES y EF). El fin del proyecto es el mayor EF. Hacia atrás, en orden inverso: cada tarea termina lo más tarde que le permiten sus sucesoras sin retrasar el proyecto (LS y LF).
La diferencia es la holgura total (en MS Project, «margen de demora total»): cuánto puede retrasarse una tarea sin mover el fin del proyecto. Las tareas con holgura 0 forman la ruta crítica. La holgura libre («margen de demora permisible») es cuánto puede retrasarse sin mover a ninguna sucesora. Cambia las duraciones:
Duraciones (días)
| Tarea | Holgura total | Holgura libre | ¿Crítica? |
|---|---|---|---|
| A | 0 d | 0 d | sí |
| B | 3 d | 2 d | no |
| C | 0 d | 0 d | sí |
| D | 1 d | 1 d | no |
| E | 0 d | 0 d | sí |
| F | 0 d | 0 d | sí |
El proyecto dura 11 días laborables: lo que marca la cadena más larga. Alarga una tarea no crítica más allá de su holgura total y verás cómo la ruta crítica cambia de camino.
for (const v of orden) // hacia adelante
es[v] = max(inicioProyecto, ...predecesoras.map(p => ef[p] + retraso))
ef[v] = es[v] + duración[v]
for (const v of orden.reverse()) // hacia atrás
lf[v] = min(finProyecto, ...sucesoras.map(s => ls[s] - retraso))
ls[v] = lf[v] - duración[v]
holguraTotal[v] = lf[v] - ef[v]; crítica[v] = holguraTotal[v] <= 0En el código real todo vive en Float64Array y las aristas en listas compactas (CSR): un array con dónde empiezan las aristas de cada nodo y otro con las aristas seguidas. Sin objetos por tarea en el bucle, el recálculo de 10.000 tareas cabe en un cuadro de pantalla.
5. Tipos de vínculo
Los vínculos no siempre son «termina A, empieza B». En la versión en español se escriben FC (fin a comienzo), CC, FF y CF, con un adelanto (negativo) o retraso en tiempo o en porcentaje de la predecesora: 3FC+2 días, 5CC-25%. Cada tipo es una desigualdad distinta entre fechas, y todas se convierten en un límite para el comienzo de la sucesora. Esta es la tabla y el Gantt reales:
B empieza cuando A termina
También puedes arrastrar las barras o escribir en la columna Predecesoras.
FC: comienzo(B) >= fin(A) + retraso
CC: comienzo(B) >= comienzo(A) + retraso
FF: fin(B) >= fin(A) + retraso → comienzo(B) >= fin(A) + retraso - duración(B)
CF: fin(B) >= comienzo(A) + retraso → comienzo(B) >= comienzo(A) + retraso - duración(B)6. Restricciones
Además de los vínculos, una tarea puede tener una restricción de fecha. Las «blandas» empujan («No comenzar antes del», que es lo que pone MS Project cuando arrastras una barra o escribes un comienzo). Las «duras» («Debe comenzar el», «No finalizar después del») ganan a los vínculos: la tarea se queda en su fecha aunque eso contradiga a una predecesora, se marca el conflicto con un indicador y la holgura queda negativa, que es la forma de decir «no llegas».
La fecha límite no mueve nada: solo limita las fechas tardías en la pasada hacia atrás (y avisa si se pasa). «Lo más tarde posible» se aplica al final: la tarea se corre hacia la derecha tanto como su holgura libre permita, de la última a la primera para que una cadena de tareas así se mueva entera.
7. Recursos
Con el cronograma ya calculado, el trabajo de cada asignación se reparte por días: lo que la tarea trabaja ese día según el calendario, por las unidades asignadas (un plomero al 50 % trabaja 4 horas de cada 8). Un recurso está sobreasignado un día si su trabajo supera su capacidad: horas laborables del día por su capacidad máxima. MS Project lo calcula minuto a minuto; por días es suficiente para verlo en el Gráfico de recursos y, en el nivel 3, para nivelar.
Los recursos no cambian las fechas: MS Project tampoco lo hace salvo que se nivele. Por eso el cálculo de recursos va después del CPM y es otra función pura, resourceUsage(proyecto, cronograma).
Nivelar («Redistribuir todo») es retrasar tareas hasta que nadie trabaje de más. Hacerlo de forma óptima es NP-difícil, así que se usa la heurística clásica de generación en serie: se vuelve a ejecutar el mismo CPM, pero colocando las tareas de una en una (entre las disponibles, la de comienzo tardío más temprano) en el primer hueco en que sus recursos tienen capacidad durante toda su duración. Cada tarea se coloca una sola vez. La primera versión, que retrasaba tareas día a día y volvía a programar, se quedaba en bucle: la tarea que se «quedaba» la movía después su predecesora y el conflicto se repetía sin fin.
schedule(proyecto, calendario, {
priority: comienzoTardío, // Kahn con montículo: la más urgente primero
place: (tarea, comienzo, duración) => {
while (!cabe(tarea, comienzo, duración)) comienzo = inicioDelDíaSiguiente(primerConflicto)
reservar(tarea, comienzo, duración) // perfil de carga por recurso y día
return comienzo // el CPM sigue con este comienzo
},
})8. El Gantt en Canvas
La tabla es HTML (necesita foco, edición, selección de texto); el Gantt es un <canvas>. Como todas las filas miden lo mismo, la primera fila visible es scrollTop / 24 y solo se pintan esas: con 10.000 tareas se dibujan unas 40 barras y los vínculos que cruzan la pantalla. La tabla usa el mismo truco (solo existen en el DOM las filas visibles) y las dos comparten un único scrollTop en el estado.
El eje X es tiempo transcurrido: x = (fecha − origen) / 1440 × píxeles por día − desplazamiento. El zoom con Ctrl+rueda cambia los píxeles por día manteniendo fija la fecha bajo el cursor, y la escala elige sus niveles (horas, días, semanas, meses, trimestres) según cuánto espacio queda para cada marca. Pruébalo con el proyecto grande:
9. Estado y deshacer
Como en Planos, el estado vive fuera de React en stores de Zustand vanilla, ahora tres:
- Documento: tareas, vínculos, recursos, calendario, línea base. Se guarda y se deshace.
- Derivado: el cronograma, las filas visibles, el uso de recursos. Se recalcula entero tras cada cambio del documento; nunca entra en el historial.
- Sesión: selección, edición, zoom, arrastres. Ni se guarda ni se deshace.
Cada edición pasa por produceWithPatches de Immer, que devuelve la lista exacta de lo que cambió y la lista inversa. El historial guarda esas listas, no copias de 10.000 tareas. Edita algo y mira el último paso:
Historial (0)
Escribe una duración, arrastra una barra o sangra una tarea (Alt+Mayús+→).
10. Mapa de archivos
engine/ lógica pura, sin React ni DOM (con tests y benchmark)
types.ts el modelo: tareas, vínculos, recursos, calendario
dates.ts minutos de pared, formato «lun 02/11/26»
calendar.ts tiempo laborable ↔ fechas
duration.ts «5d?», «2 sem», «+25%»
links.ts columna Predecesoras («3FC+2 días»)
outline.ts esquema: sangría, EDT, subárboles
schedule.ts el CPM
resources.ts uso de recursos y sobreasignación
validate.ts reglas de MS Project para vínculos
samples.ts la vivienda y el generador de 10.000 tareas
state/
document.ts Zustand + Immer: el proyecto y su historial
editor.ts derivados, sesión y todas las operaciones
columns.ts las tablas (Entrada, Programación, Seguimiento…)
cellText.ts el texto de cada celda
clipboard.ts copiar y pegar filas y TSV
taskInfo.ts el formulario de Información de la tarea
persistence.ts autoguardado en IndexedDB
view/
timeline.ts escala de tiempo: zoom, niveles, marcas
ganttLayout.ts barras, flechas, hit-testing
renderer.ts Canvas 2D
components/ la interfaz de React
learn/ estas demos