Creación de un componente de diagrama de Gantt interactivo con Vanilla JavaScript (Parte 1)

 

 

 

  • Patrones de diseño de interfaces inteligentes, vídeo de 10h + formación UX
  • ¡Registro!

  • Índice
    1. Archivos de muestra e instrucciones para ejecutar el código
  • Estructura básica del componente web
  • Representación del diagrama de Gantt con JavaScript y CSS Grid
    1. Representando la cuadrícula
    2. Representar los trabajos
  • Integración del componente del diagrama de Gantt en su aplicación
  • panorama
  • Con un diagrama de Gantt, puedes visualizar horarios y asignar tareas. En este artículo, codificaremos un diagrama de Gantt como un componente web reutilizable. Nos centraremos en la arquitectura del componente, renderizando el calendario con CSS Grid y gestionando el estado de las tareas arrastrables con JavaScript Proxy Objects.

     

    Si trabaja con datos de tiempo en su aplicación, una visualización gráfica como un calendario o un diagrama de Gantt suele resultar muy útil. A primera vista, desarrollar su propio componente gráfico parece bastante complicado. Por lo tanto, en este artículo desarrollaré las bases para un componente de diagrama de Gantt cuya apariencia y funcionalidad puedes personalizar para cualquier caso de uso.

    Estas son las características básicas del diagrama de Gantt que me gustaría implementar:

    • El usuario puede elegir entre dos vistas: año/mes o mes/día.
    • El usuario puede definir el horizonte de planificación seleccionando una fecha de inicio y una fecha de finalización.
    • El gráfico muestra una lista determinada de trabajos que se pueden mover arrastrando y soltando. Los cambios se reflejan en el estado de los objetos.
    • A continuación puede ver el diagrama de Gantt resultante en ambas vistas. En la versión mensual he incluido tres trabajos a modo de ejemplo.

    Diagrama de Gantt con vista mensual. ( Vista previa grande )
    Diagrama de Gantt con vista diaria. ( Vista previa grande )

     

    A continuación puede ver el diagrama de Gantt resultante en ambas vistas. En la versión mensual he incluido tres trabajos a modo de ejemplo.

    Archivos de muestra e instrucciones para ejecutar el código

    Puede encontrar los fragmentos de código completos de este artículo en los siguientes archivos:

    • índice.html
    • index.js
    • VanillaGanttChart.js
    • AñoMesRenderer.js
    • DateTimeRenderer.js .

    Dado que el código contiene módulos JavaScript, sólo puede ejecutar el ejemplo desde un servidor HTTP y no desde el sistema de archivos local. Para realizar pruebas en su PC local, recomendaría el módulo live-server , que puede instalar a través de npm.

    Alternativamente, puede probar el ejemplo aquí directamente en su navegador sin instalación.

    Estructura básica del componente web

    Decidí implementar el diagrama de Gantt como componente web. Esto nos permite crear un elemento HTML personalizado , en mi caso gantt-chart/gantt-chart, que podemos reutilizar fácilmente en cualquier lugar de cualquier página HTML.

    Puede encontrar información básica sobre el desarrollo de componentes web en MDN Web Docs . El siguiente listado muestra la estructura del componente. Está inspirado en el ejemplo “contra” de Alligator.io .

    El componente define una plantilla que contiene el código HTML necesario para mostrar el diagrama de Gantt. Para conocer las especificaciones CSS completas, consulte los archivos de muestra. Los campos de selección específicos para año, mes o fecha aún no se pueden definir aquí, ya que dependen del nivel de vista seleccionado.

    En su lugar , los elementos de selección se proyectan mediante una de las dos clases de renderizador . Lo mismo se aplica a la representación del diagrama de Gantt real en el elemento con ID gantt-container, que también es manejado por la clase de representación responsable.

    La clase VanillaGanttChartahora describe el comportamiento de nuestro nuevo elemento HTML. En el constructor, primero definimos nuestra plantilla aproximada como el DOM oculto del elemento.

    El componente debe inicializarse con dos matrices , jobsy resources. La jobsmatriz contiene las tareas que se muestran en el gráfico como barras verdes móviles. La resourcesmatriz define las filas individuales del gráfico donde se pueden asignar tareas. En las capturas de pantalla anteriores, por ejemplo, tenemos 4 recursos etiquetados de Tarea 1 a Tarea 4 . Por lo tanto, los recursos pueden representar tareas individuales, pero también personas, vehículos y otros recursos físicos, lo que permite una variedad de casos de uso.

     

    Actualmente, se YearMonthRendererutiliza como renderizador predeterminado . Tan pronto como el usuario selecciona un nivel diferente, el renderizador cambia en el changeLevelmétodo: primero, los elementos DOM y los oyentes específicos del renderizador se eliminan del Shadow DOM utilizando el clearmétodo del renderizador antiguo. Luego, el nuevo renderizador se inicializa con los trabajos y recursos existentes y se inicia el renderizado.

    import {YearMonthRenderer} from './YearMonthRenderer.js';import {DateTimeRenderer} from './DateTimeRenderer.js';const template = document.createElement('template');template.innerHTML = `style … /style div select name="select-level" option value="year-month"Month / Day/option option value="day"Day / Time/option /select fieldset legendFrom/legend /fieldset fieldset legendTo/legend /fieldset /div div /div`;export default class VanillaGanttChart extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.shadowRoot.appendChild(template.content.cloneNode(true)); this.levelSelect = this.shadowRoot.querySelector('#select-level'); } _resources = []; _jobs = []; _renderer; set resources(list){…} get resources(){…} set jobs(list){…} get jobs(){…} get level() {…} set level(newValue) {…} get renderer(){…} set renderer(r){…} connectedCallback() { this.changeLevel = this.changeLevel.bind(this); this.levelSelect.addEventListener('change', this.changeLevel); this.level = "year-month"; this.renderer = new YearMonthRenderer(this.shadowRoot); this.renderer.dateFrom = new Date(2021,5,1); this.renderer.dateTo = new Date(2021,5,24); this.renderer.render(); } disconnectedCallback() { if(this.levelSelect) this.levelSelect.removeEventListener('change', this.changeLevel); if(this.renderer) this.renderer.clear(); } changeLevel(){ if(this.renderer) this.renderer.clear(); var r; if(this.level == "year-month"){ r = new YearMonthRenderer(this.shadowRoot); }else{ r = new DateTimeRenderer(this.shadowRoot); } r.dateFrom = new Date(2021,5,1); r.dateTo = new Date(2021,5,24); r.resources = this.resources; r.jobs = this.jobs; r.render(); this.renderer = r; } } window.customElements.define('gantt-chart', VanillaGanttChart);

    Antes de profundizar en el proceso de renderizado, me gustaría brindarles una descripción general de las conexiones entre los diferentes scripts:

    • index.html es tu página web donde puedes usar la etiquetagantt-chart/gantt-chart
    • index.js es un script en el que inicializa la instancia del componente web asociado con el diagrama de Gantt utilizado en index.html con los trabajos y recursos apropiados (por supuesto, también puede usar múltiples diagramas de Gantt y, por lo tanto, múltiples instancias del componente web)
    • El componente VanillaGanttChartdelega la renderización a las dos clases de renderizador YearMonthRenderery DateTimeRenderer.

    Arquitectura de componentes de nuestro ejemplo de diagrama de Gantt. ( Vista previa grande )

     

    Representación del diagrama de Gantt con JavaScript y CSS Grid

    A continuación, analizamos el proceso de renderizado utilizando YearMonthRenderercomo ejemplo. Tenga en cuenta que he utilizado la llamada función constructora en lugar de la classpalabra clave para definir la clase. Esto me permite distinguir entre propiedades públicas ( this.rendery this.clear) y variables privadas (definidas con var).

    La representación del gráfico se divide en varios subpasos:

    1. initSettings
      Representación de los controles que se utilizan para definir el horizonte de planificación.
    2. initGantt
      Representación del diagrama de Gantt, básicamente en cuatro pasos:
      • initFirstRow(dibuja 1 fila con nombres de meses)
      • initSecondRow(dibuja 1 fila con los días del mes)
      • initGanttRows(dibuja 1 fila para cada recurso con celdas de cuadrícula para cada día del mes)
      • initJobs(coloca los trabajos arrastrables en el gráfico)
    export function YearMonthRenderer(root){ var shadowRoot = root; var names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; this.resources=[]; this.jobs = []; this.dateFrom = new Date(); this.dateTo = new Date(); //select elements var monthSelectFrom; var yearSelectFrom; var monthSelectTo; var yearSelectTo; var getYearFrom = function() {…} var setYearFrom = function(newValue) {…} var getYearTo = function() {…} var setYearTo = function(newValue) {…} var getMonthFrom = function() {…} var setMonthFrom = function(newValue) {…} var getMonthTo = function() {…} var setMonthTo = function(newValue) {…} this.render = function(){ this.clear(); initSettings(); initGantt(); } //remove select elements and listeners, clear gantt-container this.clear = function(){…} //add HTML code for the settings area (select elements) to the shadow root, initialize associated DOM elements and assign them to the properties monthSelectFrom, monthSelectTo etc., initialize listeners for the select elements var initSettings = function(){…} //add HTML code for the gantt chart area to the shadow root, position draggable jobs in the chart var initGantt = function(){…} //used by initGantt: draw time axis of the chart, month names var initFirstRow = function(){…} //used by initGantt: draw time axis of the chart, days of month var initSecondRow = function(){…} //used by initGantt: draw the remaining grid of the chart var initGanttRows = function(){…}.bind(this); //used by initGantt: position draggable jobs in the chart cells var initJobs = function(){…}.bind(this); //drop event listener for jobs var onJobDrop = function(ev){…}.bind(this); //helper functions, see example files ...}

    Representando la cuadrícula

    Recomiendo CSS Grid para dibujar el área del diagrama porque facilita la creación de diseños de varias columnas que se adaptan dinámicamente al tamaño de la pantalla.

     

    En el primer paso, tenemos que determinar el número de columnas de la cuadrícula. Al hacerlo, nos referimos a la primera fila del gráfico que (en el caso de YearMonthRenderer) representa los meses individuales.Te recomendamos Betflix Apk 2025

    En consecuencia, necesitamos:

    • una columna para los nombres de los recursos, por ejemplo, con un ancho fijo de 100 px.
    • una columna para cada mes, del mismo tamaño y utilizando todo el espacio disponible.

    Esto se puede lograr con la configuración 100px repeat(${n_months}, 1fr)de la propiedad gridTemplateColumnsdel contenedor del gráfico.

    Esta es la parte inicial del initGanttmétodo:

    var container = shadowRoot.querySelector("#gantt-container");container.innerHTML = "";var first_month = new Date(getYearFrom(), getMonthFrom(), 1);var last_month = new Date(getYearTo(), getMonthTo(), 1); //monthDiff is defined as a helper function at the end of the filevar n_months = monthDiff(first_month, last_month)+1; container.style.gridTemplateColumns = `100px repeat(${n_months},1fr)`;

    En la siguiente imagen puedes ver un gráfico de dos meses con n_months=2:

    El gráfico de 2 meses, configurado con n_months=2. ( Vista previa grande )

    Una vez que hayamos definido las columnas exteriores, podemos comenzar a llenar la cuadrícula . Sigamos con el ejemplo de la imagen de arriba. En la primera fila inserto 3 divs con las clases gantt-row-resourcey gantt-row-period. Puede encontrarlos en el siguiente fragmento del inspector DOM.

    En la segunda fila, uso las mismas tres divs para mantener la alineación vertical. Sin embargo, el mes divobtiene elementos secundarios para los días individuales del mes.

    div div/div divJun 2021/div divJul 2021/div div/div div div1/div div2/div div3/div div4/div div5/div div6/div div7/div div8/div div9/div div10/div ... /div .../div

    Para que los elementos secundarios también se organicen horizontalmente, necesitamos la configuración display: gridde la clase gantt-row-period. Además, no sabemos exactamente cuántas columnas se requieren para cada mes individual (28, 30 o 31). Por lo tanto, uso la configuración grid-auto-columns. Con el valor minmax(20px, 1fr);puedo asegurar que se mantenga un ancho mínimo de 20px y que de lo contrario se aproveche al máximo el espacio disponible:

    #gantt-container { display: grid;}.gantt-row-resource { background-color: whitesmoke; color: rgba(0, 0, 0, 0.726); border: 1px solid rgb(133, 129, 129); text-align: center;}.gantt-row-period { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(20px, 1fr); background-color: whitesmoke; color: rgba(0, 0, 0, 0.726); border: 1px solid rgb(133, 129, 129); text-align: center;}

    Las filas restantes se generan según la segunda fila, pero como celdas vacías .

     

    Aquí está el código JavaScript para generar las celdas de la cuadrícula individuales de la primera fila. Los métodos initSecondRowy initGanttRowstienen una estructura similar.

    var initFirstRow = function(){ if(checkElements()){ var container = shadowRoot.querySelector("#gantt-container"); var first_month = new Date(getYearFrom(), getMonthFrom(), 1); var last_month = new Date(getYearTo(), getMonthTo(), 1); var resource = document.createElement("div"); resource.className = "gantt-row-resource"; container.appendChild(resource); var month = new Date(first_month); for(month; month = last_month; month.setMonth(month.getMonth()+1)){ var period = document.createElement("div"); period.className = "gantt-row-period"; period.innerHTML = names[month.getMonth()] + " " + month.getFullYear(); container.appendChild(period); } }}

    Representar los trabajos

    Ahora cada uno jobtiene que ser dibujado en el diagrama en la posición correcta . Para esto hago uso de los atributos de datos HTML: cada celda de la cuadrícula en el área principal del gráfico está asociada con los dos atributos data-resourcee data-dateindica la posición en el eje horizontal y vertical del gráfico (ver función initGanttRowsen los archivos YearMonthRenderer.jsy DateTimeRenderer.js).

    Como ejemplo, veamos las primeras cuatro celdas de la cuadrícula en la primera fila del gráfico (todavía estamos usando el mismo ejemplo que en las imágenes de arriba):

    Centrándose en las primeras cuatro celdas de la cuadrícula en la primera fila del gráfico. ( Vista previa grande )

    En el inspector DOM puedes ver los valores de los atributos de datos que he asignado a las celdas individuales:

    Se asignan los valores para los atributos de datos. ( Vista previa grande )

    Veamos ahora qué significa esto para la función initJobs. Con la ayuda de la función querySelector, ahora es bastante fácil encontrar la celda de la cuadrícula en la que se debe colocar un trabajo.

    El siguiente desafío es determinar el ancho correcto de un jobelemento. Dependiendo de la vista seleccionada, cada celda de la cuadrícula representa una unidad de un día (nivel month/day) o una hora (nivel day/time). Dado que cada trabajo es el elemento secundario de una celda, la jobduración de 1 unidad (día u hora) corresponde a un ancho de 1*100%, la duración de 2 unidades corresponde a un ancho de 2*100%, y así sucesivamente. Esto hace posible utilizar la calcfunción CSS para establecer dinámicamente el ancho de un jobelemento , como se muestra en el siguiente listado.

    var initJobs = function(){ this.jobs.forEach(job = { var date_string = formatDate(job.start); var ganttElement = shadowRoot.querySelector(`div[data-resource="${job.resource}"][data-date="${date_string}"]`); if(ganttElement){ var jobElement = document.createElement("div"); jobElement.className="job"; jobElement.id = job.id; //helper function dayDiff - get difference between start and end in days var d = dayDiff(job.start, job.end); //d -- number of grid cells covered by job + sum of borderWidths jobElement.style.width = "calc("+(d*100)+"% + "+ d+"px)"; jobElement.draggable = "true"; jobElement.ondragstart = function(ev){ //the id is used to identify the job when it is dropped ev.dataTransfer.setData("job", ev.target.id); }; ganttElement.appendChild(jobElement); } }); }.bind(this);

    Para poder arrastrar un job archivo , se requieren tres pasos:

     

    • Establezca la propiedad draggabledel elemento de trabajo en true(consulte la lista anterior).
    • Defina un controlador de eventos para el evento ondragstartdel elemento de trabajo (consulte la lista anterior).
    • Defina un controlador de eventos para el evento ondropde las celdas de la cuadrícula del diagrama de Gantt, que son los posibles destinos de colocación del elemento de trabajo (consulte la función initGanttRowsen el archivo YearMonthRenderer.js).

    El controlador de eventos para el evento ondropse define de la siguiente manera:

    var onJobDrop = function(ev){ // basic null checks if (checkElements()) { ev.preventDefault(); // drop target = grid cell, where the job is about to be dropped var gantt_item = ev.target; // prevent that a job is appended to another job and not to a grid cell if (ev.target.classList.contains("job")) { gantt_item = ev.target.parentNode; } // identify the dragged job var data = ev.dataTransfer.getData("job"); var jobElement = shadowRoot.getElementById(data); // drop the job gantt_item.appendChild(jobElement); // update the properties of the job object var job = this.jobs.find(j = j.id == data ); var start = new Date(gantt_item.getAttribute("data-date")); var end = new Date(start); end.setDate(start.getDate()+dayDiff(job.start, job.end)); job.start = start; job.end = end; job.resource = gantt_item.getAttribute("data-resource"); } }.bind(this);

    De este modo, todos los cambios realizados en los datos del trabajo mediante arrastrar y soltar se reflejan en la lista jobsdel componente del diagrama de Gantt.

    Integración del componente del diagrama de Gantt en su aplicación

    Puedes usar la etiqueta gantt-chart/gantt-charten cualquier lugar de los archivos HTML de tu aplicación (en mi caso en el archivo index.html) bajo las siguientes condiciones:

    • El script VanillaGanttChart.jsdebe integrarse como módulo para que la etiqueta se interprete correctamente.
    • Necesita un script separado en el que se inicialice el diagrama de Gantt con jobsy resources(en mi caso, el archivo index.js).
    !DOCTYPE htmlhtml head meta charset="UTF-8"/ titleGantt chart - Vanilla JS/title script type="module" src="VanillaGanttChart.js"/script /head body gantt-chart/gantt-chart script type="module" src="index.js"/script /body /html

    Por ejemplo, en mi caso el archivo index.jstiene el siguiente aspecto:

     

    import VanillaGanttChart from "./VanillaGanttChart.js"; var chart = document.querySelector("#g1"); chart.jobs = [ {id: "j1", start: new Date("2021/6/1"), end: new Date("2021/6/4"), resource: 1}, {id: "j2", start: new Date("2021/6/4"), end: new Date("2021/6/13"), resource: 2}, {id: "j3", start: new Date("2021/6/13"), end: new Date("2021/6/21"), resource: 3},]; chart.resources = [{id:1, name: "Task 1"}, {id:2, name: "Task 2"}, {id:3, name: "Task 3"}, {id:4, name: "Task 4"}];

    Sin embargo, todavía queda un requisito pendiente: cuando el usuario realiza cambios arrastrando trabajos en el diagrama de Gantt, los cambios respectivos en los valores de las propiedades de los trabajos deben reflejarse en la lista fuera del componente.

    Podemos lograr esto con el uso de objetos proxy de JavaScript : cada uno jobestá anidado en un objeto proxy , al que proporcionamos el llamado validador . Se activa tan pronto como se cambia una propiedad del objeto (función setdel validador) o se recupera (función getdel validador). En la función set del validador, podemos almacenar código que se ejecuta cada vez que se cambia la hora de inicio o el recurso de una tarea.

    La siguiente lista muestra una versión diferente del archivo index.js. Ahora se asigna una lista de objetos proxy al componente del diagrama de Gantt en lugar de a los trabajos originales. En el validador setutilizo una salida de consola simple para mostrar que me han notificado un cambio de propiedad.

    import VanillaGanttChart from "./VanillaGanttChart.js"; var chart = document.querySelector("#g1"); var jobs = [ {id: "j1", start: new Date("2021/6/1"), end: new Date("2021/6/4"), resource: 1}, {id: "j2", start: new Date("2021/6/4"), end: new Date("2021/6/13"), resource: 2}, {id: "j3", start: new Date("2021/6/13"), end: new Date("2021/6/21"), resource: 3},];var p_jobs = []; chart.resources = [{id:1, name: "Task 1"}, {id:2, name: "Task 2"}, {id:3, name: "Task 3"}, {id:4, name: "Task 4"}]; jobs.forEach(job = { var validator = { set: function(obj, prop, value) { console.log("Job " + obj.id + ": " + prop + " was changed to " + value); console.log(); obj[prop] = value; return true; }, get: function(obj, prop){ return obj[prop]; } }; var p_job = new Proxy(job, validator); p_jobs.push(p_job);}); chart.jobs = p_jobs;

    panorama

    El diagrama de Gantt es un ejemplo que muestra cómo se pueden utilizar las tecnologías de Web Components, CSS Grid y JavaScript Proxy para desarrollar un elemento HTML personalizado con una interfaz gráfica algo más compleja. Le invitamos a desarrollar más el proyecto y/o utilizarlo en sus propios proyectos junto con otros marcos de JavaScript.

    Nuevamente, puede encontrar todos los archivos de muestra y las instrucciones en la parte superior del artículo.

    (vf, yk, il)Explora más en

    • javascript
    • Marcos
    • CSS
    • Herramientas





    Tal vez te puede interesar:

    1. Creación de su propia biblioteca de validación de React: las características (Parte 2)
    2. Introducción a Quasar Framework: creación de aplicaciones multiplataforma
    3. Creación de un componente web retro que se puede arrastrar con iluminación
    4. Creación y acoplamiento de una aplicación Node.js con arquitectura sin estado con la ayuda de Kinsta

    Creación de un componente de diagrama de Gantt interactivo con Vanilla JavaScript (Parte 1)

    Estructura básica del componente webRepresentación del diagrama de Gantt con JavaScript y CSS GridIntegración del componente del diagrama de Gantt en su apl

    programar

    es

    2025-01-15

     

    Si crees que alguno de los contenidos (texto, imagenes o multimedia) en esta página infringe tus derechos relativos a propiedad intelectual, marcas registradas o cualquier otro de tus derechos, por favor ponte en contacto con nosotros en el mail [email protected] y retiraremos este contenido inmediatamente

     

     

    Update cookies preferences