Java, UX, HTML, CSS, WEB-design

Создание компонента интерактивной диаграммы Ганта с помощью ванильного JavaScript

Краткое описание по статье Создание компонента интерактивной диаграммы Ганта с помощью ванильного JavaScript

Название: Создание компонента интерактивной диаграммы Ганта с помощью ванильного JavaScript . Краткое описание: ⭐ Анна Пренз . Дата публикации: 15.01.2022 . Автор: Алишер Валеев .

Для чего создан сайт Novosti-Nedeli.ru

Данный сайт посвящен новостям мира и мира технологий . Также тут вы найдете руководства по различным девайсам.

Сколько лет сайту?

Возраст составляет 3 года


  • Анна Прензел

  • 0 Комментарии

Создание компонента интерактивной диаграммы Ганта с помощью ванильного JavaScript

  • 14 минут чтения

  • JavaScript, Фреймворки, CSS, Инструменты

Краткое резюме ↬

С помощью диаграммы Ганта вы можете визуализировать графики и назначать задачи. В этой статье мы закодируем диаграмму Ганта как многоразовый веб-компонент. Мы сосредоточимся на архитектуре компонента, рендеринге календаря с помощью CSS Grid и управлении состоянием перетаскиваемых задач с помощью прокси-объектов JavaScript.

Если вы работаете с временными данными в своем приложении, графическая визуализация в виде календаря или диаграммы Ганта часто оказывается очень полезной. На первый взгляд разработка собственного компонента диаграммы кажется довольно сложной задачей. Поэтому в этой статье я разработаю основу для Компонент диаграммы Ганта внешний вид и функциональность которого можно настроить для любого варианта использования.

Эти основные возможности диаграммы Ганта что я хотел бы реализовать:

  • Пользователь может выбирать между двумя представлениями: год/месяц или месяц/день.
  • Пользователь может определить горизонт планирования, выбрав дату начала и дату окончания.
  • На диаграмме отображается заданный список заданий, которые можно перемещать путем перетаскивания. Изменения отражаются на состоянии объектов.
  • Ниже вы можете увидеть получившуюся диаграмму Ганта в обоих представлениях. В ежемесячную версию я включил в качестве примера три работы.

Диаграмма Ганта с месячным обзором

Диаграмма Ганта с месячным обзором. (Большой превью)

Диаграмма Ганта с дневным обзором

Диаграмма Ганта с дневным обзором. (Большой превью)

Ниже вы можете увидеть получившуюся диаграмму Ганта в обоих представлениях. В ежемесячную версию я включил в качестве примера три работы.

Примеры файлов и инструкции по запуску кода

Вы можете найти полные фрагменты кода этой статьи в следующих файлах:

  • index.html
  • index.js
  • VanillaGanttChart.js
  • ГодМесяцRenderer.js
  • DateTimeRenderer.js.

Поскольку код содержит модули JavaScript, вы можете запустить пример только из HTTP-сервер а не из локальной файловой системы. Для тестирования на локальном ПК я бы рекомендовал модуль live-server, который можно установить через npm.

Кроме того, вы можете попробовать этот пример прямо в браузере без установки.

Еще после прыжка! Продолжить чтение ниже ↓

Базовая структура веб-компонента

Я решил реализовать диаграмму Ганта в виде веб-компонента. Это позволяет нам создать пользовательский HTML-элемент, в моем случае <gantt-chart></gantt-chart>, который мы можем легко повторно использовать в любом месте любой HTML-страницы.

Вы можете найти некоторую базовую информацию о разработке веб-компонентов в веб-документах MDN. В следующем листинге показана структура компонента. Он вдохновлен примером «счетчика» от Alligator.io.

Компонент определяет шаблон содержащий HTML-код, необходимый для отображения диаграммы Ганта. Полные спецификации CSS см. в примерах файлов. Конкретные поля выбора для года, месяца или даты здесь еще не могут быть определены, так как они зависят от выбранного уровня представления.

Элементы выделения проецируются одним из двух классы рендерера вместо. То же самое относится к рендерингу фактической диаграммы Ганта в элемент с идентификатором gantt-container, который также обрабатывается ответственным классом рендерера.

Класс VanillaGanttChart теперь описывает поведение нашего нового HTML-элемента. В конструкторе мы сначала определяем наш грубый шаблон как теневой DOM элемента.

Компонент должен быть инициализирован с помощью два массива, jobs, и resources. То jobs Массив содержит задачи, которые отображаются на диаграмме в виде подвижных зеленых полос. То resources Массив определяет отдельные строки в диаграмме, где могут быть назначены задачи. Например, на скриншотах выше у нас есть 4 ресурса с пометкой Задание 1 к Задача 4. Таким образом, ресурсы могут представлять отдельные задачи, а также людей, транспортные средства и другие физические ресурсы, допуская различные варианты использования.

В настоящее время YearMonthRenderer используется в качестве средство визуализации по умолчанию. Как только пользователь выбирает другой уровень, средство визуализации изменяется в changeLevel метод: во-первых, элементы DOM и слушатели, относящиеся к рендереру, удаляются из Shadow DOM с помощью clear метод старого рендерера. Затем новый модуль рендеринга инициализируется с существующими заданиями и ресурсами, и начинается рендеринг.

import {YearMonthRenderer} from './YearMonthRenderer.js';
import {DateTimeRenderer} from './DateTimeRenderer.js';

const template = document.createElement('template');

template.innerHTML = 
 `<style> … </style>

  <div id="gantt-settings">

    <select name="select-level" id="select-level">
      <option value="year-month">Month / Day</option>
      <option value="day">Day / Time</option>
    </select>

    <fieldset id="select-from">
      <legend>From</legend>
    </fieldset>

    <fieldset id="select-to">
      <legend>To</legend>
    </fieldset>
  </div>

  <div id="gantt-container">
  </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);

Прежде чем мы углубимся в процесс рендеринга, я хотел бы дать вам обзор связей между различными скриптами:

  • index.html ваша веб-страница, на которой вы можете использовать тег <gantt-chart></gantt-chart>
  • index.js — это сценарий, в котором вы инициализируете экземпляр веб-компонента, связанного с диаграммой Ганта, используемой в index.html, с соответствующими заданиями и ресурсами (конечно, вы также можете использовать несколько диаграмм Ганта и, следовательно, несколько экземпляров веб-компонента).
  • Компонент VanillaGanttChart делегирует рендеринг двум классам рендерера YearMonthRenderer и DateTimeRenderer.

Компонентная архитектура нашего примера диаграммы Ганта

Компонентная архитектура нашего примера диаграммы Ганта. (Большой превью)

Визуализация диаграммы Ганта с помощью JavaScript и CSS Grid

Далее мы обсудим процесс рендеринга с помощью YearMonthRenderer Например. Обратите внимание, что я использовал так называемую функцию-конструктор вместо class ключевое слово для определения класса. Это позволяет мне различать общедоступные свойства (this.render и this.clear) и частные переменные (определенные с помощью var).

Визуализация диаграммы разбита на несколько подэтапов:

  1. initSettings

    Отображение элементов управления, которые используются для определения горизонта планирования.

  2. initGantt

    Рендеринг диаграммы Ганта, в основном в четыре этапа:

    • initFirstRow (рисует 1 ряд с названиями месяцев)
    • initSecondRow (рисует 1 строку с днями месяца)
    • initGanttRows (рисует по 1 строке для каждого ресурса с ячейками сетки для каждого дня месяца)
    • initJobs (помещает перетаскиваемые задания на диаграмму)
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
   ...
}

Рендеринг сетки

Я рекомендую CSS Grid для рисования области диаграммы, потому что его очень легко создавать. многоколоночные макеты которые динамически адаптируются к размеру экрана.

На первом этапе мы должны определить Число столбцов сетки. При этом мы обращаемся к первой строке диаграммы, которая (в случае YearMonthRenderer) представляет отдельные месяцы.

Следовательно, нам нужно:

  • один столбец для названий ресурсов, например, с фиксированной шириной 100px.
  • по одному столбцу на каждый месяц одинакового размера и с использованием всего доступного места.

Этого можно добиться установкой 100px repeat(${n_months}, 1fr) для собственности gridTemplateColumns контейнера диаграммы.

Это начальная часть initGantt метод:

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 file
var n_months =  monthDiff(first_month, last_month)+1;
 
container.style.gridTemplateColumns = `100px repeat(${n_months},1fr)`;

На следующем рисунке вы можете увидеть график за два месяца с n_months=2:

График на 2 месяца, установленный с n_months=2. (Большой превью)

После того, как мы определили внешние столбцы, мы можем начать заполнение сетки. Давайте остановимся на примере с картинки выше. В первую строку я вставляю 3 divс классами gantt-row-resource и gantt-row-period. Вы можете найти их в следующем фрагменте из инспектора DOM.

Во втором ряду я использую те же три divs, чтобы сохранить вертикальное выравнивание. Тем не менее, месяц divs получить дочерние элементы для отдельных дней месяца.

<div id="gantt-container"
  style="grid-template-columns: 100px repeat(2, 1fr);">
  <div class="gantt-row-resource"></div>
  <div class="gantt-row-period">Jun 2021</div>
  <div class="gantt-row-period">Jul 2021</div>
  <div class="gantt-row-resource"></div>
  <div class="gantt-row-period">
    <div class="gantt-row-period">1</div>
    <div class="gantt-row-period">2</div>
    <div class="gantt-row-period">3</div>
    <div class="gantt-row-period">4</div>
    <div class="gantt-row-period">5</div>
    <div class="gantt-row-period">6</div>
    <div class="gantt-row-period">7</div>
    <div class="gantt-row-period">8</div>
    <div class="gantt-row-period">9</div>
    <div class="gantt-row-period">10</div>
  ...
  </div>
  ...
</div>

Чтобы дочерние элементы располагались также горизонтально, нам нужна настройка display: grid для класса gantt-row-period. Кроме того, мы точно не знаем, сколько столбцов требуется для отдельных месяцев (28, 30 или 31). Поэтому я использую настройку grid-auto-columns. Со значением minmax(20px, 1fr); Я могу убедиться, что поддерживается минимальная ширина 20 пикселей, а в противном случае доступное пространство используется полностью:

#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;
}

Остальные строки генерируются в соответствии со второй строкой, однако как пустые ячейки.

Вот код JavaScript для создания отдельных ячеек сетки первой строки. Методы initSecondRow и initGanttRows имеют схожую структуру.

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);
        }
  }
}

Рендеринг заданий

Теперь каждый job необходимо начертить на схеме правильное положение. Для этого я использую атрибуты данных HTML: каждая ячейка сетки в основной области диаграммы связана с двумя атрибутами. data-resource и data-date с указанием положения по горизонтальной и вертикальной оси диаграммы (см. функцию initGanttRows в файлах YearMonthRenderer.js и DateTimeRenderer.js).

В качестве примера рассмотрим первые четыре ячейки сетки в первой строке диаграммы (мы все еще используем тот же пример, что и на рисунках выше):

Сосредоточьтесь на первых четырех ячейках сетки в первой строке диаграммы. (Большой превью)

В инспекторе DOM вы можете увидеть значения атрибутов данных, которые я присвоил отдельным ячейкам:

Присваиваются значения для атрибутов данных. (Большой превью)

Давайте теперь посмотрим, что это означает для функции initJobs. С помощью функции querySelector, теперь довольно легко найти ячейку сетки, в которую следует поместить задание.

Следующая задача состоит в том, чтобы определить правильную ширину для job элемент. В зависимости от выбранного вида каждая ячейка сетки представляет единица одного дня (уровень month/day) или один час (уровень day/time). Поскольку каждое задание является дочерним элементом ячейки, job продолжительность 1 единицы (день или час) соответствует ширине 1*100%, длительность 2 единицы соответствует ширине 2*100%, и так далее. Это позволяет использовать CSS calc функционировать, чтобы динамически устанавливать ширину job элемент, как показано в следующем листинге.

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);

Чтобы сделать job перетаскиваемый, необходимо выполнить три шага:

  • Установите свойство draggable элемента работы к true (см. список выше).
  • Определить обработчик события для события ondragstart элемента job (см. листинг выше).
  • Определить обработчик события для события ondrop для ячеек сетки диаграммы Ганта, которые являются возможными целями перетаскивания элемента задания (см. функцию initGanttRows в файле YearMonthRenderer.js).

Обработчик события для события ondrop определяется следующим образом:

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);

Таким образом, все изменения в данных задания, сделанные путем перетаскивания, отражаются в списке. jobs компонента диаграммы Ганта.

Интеграция компонента диаграммы Ганта в ваше приложение

Вы можете использовать тег <gantt-chart></gantt-chart> в любом месте файлов HTML вашего приложения (в моем случае в файле index.html) при следующих условиях:

  • Сценарий VanillaGanttChart.js должен быть интегрирован как модуль, чтобы тег интерпретировался правильно.
  • Вам нужен отдельный скрипт, в котором диаграмма Ганта инициализируется с помощью jobs и resources (в моем случае файл index.js).
<!DOCTYPE html>
<html>
 <head>
   <meta charset="UTF-8"/>
   <title>Gantt chart - Vanilla JS</title>
   <script type="module" src="https://www.smashingmagazine.com/2021/08/interactive-gantt-chart-component-vanilla-javascript/VanillaGanttChart.js"></script>   
 </head>
    
 <body>
 
  <gantt-chart id="g1"></gantt-chart> 
 
  <script type="module" src="index.js"></script>
 </body> 
</html>

Например, в моем случае файл index.js выглядит следующим образом:

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"}];

Однако остается открытым одно требование: когда пользователь вносит изменения, перетаскивая задания на диаграмме Ганта, соответствующие изменения в значениях свойств заданий должны отражаться в списке. за пределами компонент.

Мы можем добиться этого с помощью прокси-объектов JavaScript: каждый job вложен в прокси-объект, который мы предоставляем с так называемым валидатором. Он становится активным, как только изменяется свойство объекта (функция set валидатора) или извлечены (функция get валидатора). В функции set валидатора мы можем хранить код, который выполняется всякий раз, когда изменяется время запуска или ресурс задачи.

В следующем листинге показана другая версия файла index.js. Теперь вместо исходных заданий компоненту диаграммы Ганта назначается список прокси-объектов. В валидаторе set Я использую простой вывод консоли, чтобы показать, что я был уведомлен об изменении свойства.

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;

Перспектива

Диаграмма Ганта — это пример, показывающий, как вы можете использовать технологии веб-компонентов, CSS Grid и JavaScript Proxy для разработки пользовательский HTML-элемент с несколько более сложным графическим интерфейсом. Вы можете развивать проект дальше и/или использовать его в своих собственных проектах вместе с другими фреймворками JavaScript.

Опять же, вы можете найти все образцы файлов и инструкции вверху статьи.

Сокрушительная редакция
(вф, ык, иль)




Source: https://smashingmagazine.com

Заключение

Вы ознакомились с статьей — Создание компонента интерактивной диаграммы Ганта с помощью ванильного JavaScript

Пожалуйста оцените статью, и напишите комментарий.

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован.

Кнопка «Наверх»