با نمودار گانت ، می توانید برنامه ها را مجسم کنید و وظایف را تعیین کنید. در این مقاله ، ما نمودار گانت را به عنوان یک جزء وب قابل استفاده مجدد کدگذاری می کنیم. ما بر معماری جزء ، ارائه تقویم با CSS Grid و مدیریت وضعیت کارهای قابل کشیدن با JavaScript Proxy Objects تمرکز می کنیم.
اگر با داده های زمان در برنامه خود کار می کنید ، تجسم گرافیکی به عنوان تقویم یا نمودار گانت اغلب بسیار مفید است. در نگاه اول ، توسعه جزء نمودار خود بسیار پیچیده به نظر می رسد. بنابراین ، در این مقاله ، من پایه ای برای a ایجاد می کنم جزء نمودار گانت ظاهر و عملکرد آن را می توانید برای هر مورد استفاده سفارشی کنید.
اینها هستند ویژگی های اصلی نمودار گانت که می خواهم پیاده سازی کنم:
- کاربر می تواند بین دو نمای انتخاب کند: سال/ماه یا ماه/روز.
- کاربر می تواند افق برنامه ریزی را با انتخاب تاریخ شروع و تاریخ پایان تعریف کند.
- نمودار لیستی از مشاغل را ارائه می دهد که می توان آنها را با کشیدن و رها کردن جابجا کرد. تغییرات در وضعیت اجسام منعکس می شود.
-
در زیر می توانید نمودار گانت حاصل را در هر دو نمای مشاهده کنید. در نسخه ماهانه ، من سه شغل را به عنوان مثال گنجانده ام.
نمودار گانت با نمای ماه (پیش نمایش بزرگ)
نمودار گانت با نمای روز. (پیش نمایش بزرگ)
در زیر می توانید نمودار گانت حاصل را در هر دو نمای مشاهده کنید. در نسخه ماهانه ، من سه شغل را به عنوان مثال گنجانده ام.
نمونه فایلها و دستورالعملهای اجرای کد
می توانید پیدا کنید قطعات کامل کد این مقاله در فایلهای زیر:
از آنجا که کد شامل ماژول های جاوا اسکریپت است ، فقط می توانید مثال را از یک اجرا کنید سرور HTTP و نه از سیستم فایل محلی. برای آزمایش روی رایانه محلی خود ، ماژول را توصیه می کنم سرور زنده، که می توانید از طریق npm نصب کنید.
متناوبا ، می توانید مثال را مستقیماً در مرورگر خود امتحان کنید بدون نصب
ساختار اساسی کامپوننت وب
تصمیم گرفتم نمودار گانت را به عنوان یک جزء وب پیاده سازی کنم. این به ما اجازه می دهد تا a ایجاد کنیم عنصر HTML سفارشی، در مورد من <gantt-chart></gantt-chart>
، که به راحتی می توانیم در هر نقطه از صفحه HTML از آن استفاده مجدد کنیم.
می توانید برخی از اطلاعات اولیه در مورد توسعه اجزای وب را در MDN Web Docsبه فهرست زیر ساختار اجزا را نشان می دهد. از مثال “شمارنده” از الهام گرفته شده است Alligator.ioبه
جزء a را تعریف می کند قالب حاوی کد HTML مورد نیاز برای نمایش نمودار گانت. برای اطلاع از مشخصات کامل CSS ، لطفاً به فایل های نمونه مراجعه کنید. فیلدهای انتخاب خاص برای سال ، ماه یا تاریخ را هنوز نمی توان در اینجا تعریف کرد ، زیرا به سطح انتخاب شده نمای بستگی دارد.
عناصر انتخاب توسط یکی از دو مورد پیش بینی شده است کلاس های ارائه دهنده بجای. همین امر در مورد تبدیل نمودار واقعی Gantt به عنصر با شناسه نیز صدق می کند 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 یک اسکریپت است که در آن شما نمونه اجزای وب را که با نمودار Gantt استفاده شده در index.html همراه است ، با مشاغل و منابع مناسب (البته می توانید از چندین نمودار Gantt و در نتیجه چندین نمونه از جزء وب نیز استفاده کنید)
- جزء
VanillaGanttChart
نمایندگان را به دو کلاس ارائه کننده ارائه می دهدYearMonthRenderer
وDateTimeRenderer
به
معماری اجزای مثال نمودار گانت ما. (پیش نمایش بزرگ)
ارائه نمودار Gantt با JavaScript و CSS Grid
در ادامه ، ما در مورد فرایند ارائه با استفاده از YearMonthRenderer
به عنوان مثال. لطفاً توجه داشته باشید که من از یک اصطلاح استفاده کرده ام عملکرد سازنده به جای class
کلید واژه برای تعریف کلاس این به من امکان می دهد بین اموال عمومی تمایز قائل شوم (this.render
و this.clear
) و متغیرهای خصوصی (تعریف شده با var
)
ارائه نمودار به چند مرحله فرعی تقسیم می شود:
initSettings
ارائه کنترل هایی که برای تعریف افق برنامه ریزی استفاده می شوند.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
) نشان دهنده ماههای فردی است.
در نتیجه ، ما نیاز داریم:
- یک ستون برای نام منابع ، به عنوان مثال با عرض ثابت 100 پیکسل.
- یک ستون برای هر ماه ، با همان اندازه و با استفاده از فضای کامل موجود.
این را می توان با تنظیمات به دست آورد 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
s با کلاسها gantt-row-resource
و gantt-row-period
به می توانید آنها را در قطعه زیر از بازرس DOM پیدا کنید.
در ردیف دوم ، از همان سه استفاده می کنم div
s برای حفظ تراز عمودی. با این حال ، ماه div
عناصر کودک را برای روزهای جداگانه ماه دریافت کنید.
<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;
}
سطرهای باقیمانده مطابق با ردیف دوم ایجاد می شوند ، اما به صورت زیر سلول های خالیبه
در اینجا کد جاوا اسکریپت برای ایجاد تک تک سلول های ردیف اول آمده است. روش ها 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
، در حال حاضر یافتن سلول شبکه ای که کار باید در آن قرار گیرد بسیار آسان است.
چالش بعدی تعیین عرض صحیح برای a است job
عنصر بسته به نمای انتخاب شده ، هر سلول شبکه نشان دهنده a است واحد یک روز (مرحله month/day
) یا یک ساعت (سطح day/time
) از آنجا که هر شغل عنصر فرزند سلول است ، job
مدت زمان 1 واحد (روز یا ساعت) به عرض عرض مربوط می شود 1*100%
، مدت زمان 2 واحد مربوط به عرض است 2*100%
، و غیره این امکان استفاده از CSS را فراهم می کند calc
عملکرد به به صورت پویا عرض a را تنظیم کنید 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
عنصر شغل (فهرست بالا را ببینید). - برای رویداد یک کنترل کننده رویداد تعریف کنید
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://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"}];
با این حال ، هنوز یک الزام باز است: هنگامی که کاربر با کشیدن مشاغل در نمودار Gantt تغییراتی را ایجاد می کند ، تغییرات مربوطه در ارزش ویژگی های مشاغل باید در لیست منعکس شود. خارج از جزء
ما می توانیم با استفاده از اشیاء پروکسی جاوا اسکریپت: هر یک job
در a قرار دارد شیء پروکسی، که ما به اصطلاح ارائه می دهیم اعتبارسنجبه به محض تغییر ویژگی شی (عملکرد) فعال می شود set
اعتبارسنج) یا بازیابی (تابع get
اعتبارسنج) در تابع مجموعه اعتبار سنج ، می توانیم کدی را که هر زمان زمان شروع یا منبع کار تغییر می کند ، اجرا می کنیم.
فهرست زیر نسخه دیگری از فایل را نشان می دهد 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;
چشم انداز
نمودار گانت نمونه ای است که نشان می دهد چگونه می توانید از فناوری های Web Components ، CSS Grid و JavaScript Proxy برای توسعه عنصر سفارشی HTML با رابط گرافیکی تا حدودی پیچیده تر. از توسعه بیشتر پروژه و/یا استفاده از آن در پروژه های خود همراه با سایر چارچوب های جاوا اسکریپت استقبال می کنید.
مجدداً می توانید همه فایل های نمونه و دستورالعمل ها را در بالای مقاله بیابید.
