今回はJavaScriptで作成したガントチャートのバーを描画します。項目としてタスク名とカテゴリ名、担当者名、期間もいっしょに描画します。
Taskクラス
まずタスクを管理するためのTaskクラスをつくります。コンストラクタの引数のtaskName, categoryName, managerNameは文字列、startYear, startMonth, startDay, endYear, endMonth, endDayはnumber型です。
1 2 3 4 5 6 7 8 9 |
class Task { constructor(taskName, categoryName, managerName, startYear, startMonth, startDay, endYear, endMonth, endDay){ this.CategoryName = categoryName; this.TaskName = taskName; this.ManagerName = managerName; this.StartDay = new Date(startYear, startMonth-1, startDay); this.EndDay = new Date(endYear, endMonth-1, endDay); } } |
これをこんな感じで使います。Taskの配列を作成してGanttChartクラスの関数に渡せば項目とバーが表示されるわけです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>ガントチャートの実験</title> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> </head> <body> <p>2021年12月</p> <div id = "main"></div> <script src='./gantt-chart.js'></script> <script> let main_element = document.getElementById('main'); let ganttChart = new GanttChart(main_element); ganttChart.SetGanttChartFirstDay(2021,12,1); ganttChart.SetGanttChartEndDay(2021,12,31); ganttChart.DrawChart(); let tasks = []; tasks.push(new Task('タスク1-1', 'カテゴリ1', '山田太郎', 2021, 12, 3, 2021, 12, 6)); tasks.push(new Task('タスク1-2', 'カテゴリ1', '山田二郎', 2021, 12, 7, 2021, 12, 10)); tasks.push(new Task('タスク2-1', 'カテゴリ2', '山田三郎', 2021, 12, 5, 2021, 12, 8)); tasks.push(new Task('タスク2-2', 'カテゴリ2', '山田五郎', 2021, 12, 9, 2021, 12, 15)); ganttChart.SetSchedules(tasks); </script> </body> </html> |
SetSchedules関数
それではGanttChartクラスのSetSchedules関数を示します。
Taskの配列をうけとったらそれをカテゴリごとにわけます。そしてSetSchedule関数(後述)を連続して呼び出します。最後にMainElementの高さを調整します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class GanttChart{ SetSchedules(tasks){ tasks = this.GroupBy(tasks); let prevCategoryName = ''; tasks.forEach(task => { let categoryName = ''; if(prevCategoryName != task.CategoryName){ categoryName = task.CategoryName; prevCategoryName = task.CategoryName; } else categoryName = ''; this.yPos = this.SetSchedule(task.TaskName, categoryName, task.ManagerName, task.StartDay, task.EndDay, this.yPos) }); // MainElementの高さを調整する。yPosには一番下に描画された矩形の底辺のY座標が格納されている this.DayElements.forEach(element => { // DayElementsに格納されている要素は40ピクセル下から描画されているので // 高さを40ピクセル低くする element.style.height = this.yPos - 40 + 'px'; }); this.MainElement.style.height = this.yPos + 'px'; } } |
tasksに格納されているTaskをカテゴリごとにわけるGroupBy関数を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class GanttChart{ GroupBy(tasks){ const groupBy = (array, getKey) => array.reduce((obj, cur, idx, src) => { const key = getKey(cur, idx, src); (obj[key] || (obj[key] = [])).push(cur); return obj; }, {}); const groups = groupBy(tasks, task => task.CategoryName); const result = Object.entries(groups) .map(([CategoryName, list]) => ({ CategoryName, list, })); let ret = []; for(let i=0; i<result.length; i++){ for(let j=0; j<result[i].list.length; j++) ret.push(result[i].list[j]); } return ret; } } |
SetSchedule関数
SetSchedule関数はそれぞれのタスクの項目名とバーを描画する関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class GanttChart{ SetSchedule(taskName, categoryName, managerName, startDay, endDay, yPos){ // startDay、endDayを「2021 / 01 / 01」のような形で表わす // ZeroPadding関数はゼロ埋めのための関数 let startDayText = `${startDay.getFullYear()} / ${this.ZeroPadding(startDay.getMonth()+1,2)} / ${this.ZeroPadding(startDay.getDate(),2)}`; let endDayText = `${endDay.getFullYear()} / ${this.ZeroPadding(endDay.getMonth()+1,2)} / ${this.ZeroPadding(endDay.getDate(),2)}`; // 各項目の文字列と幅をそれぞれ配列に格納してSetTexts関数に渡す let widths =[this.CategoryWidth, this.TaskWidth, this.ManagerWidth, this.PeriodWidth]; let texts =[categoryName, taskName, managerName, startDayText + '<br>' + endDayText]; // 項目を描画する。戻り値は追加された要素の高さ let newElementMaxHeight = this.SetTexts(texts, widths, yPos); // バーを描画する this.DrawBand(startDay, endDay, yPos, newElementMaxHeight); // 次の項目を描画するときのY座標を返す return yPos + newElementMaxHeight; } } |
ゼロ埋めのための関数であるGanttChart関数を示します。
1 2 3 4 5 |
class GanttChart{ ZeroPadding(num,length){ return ('0000000000' + num).slice(-length); } } |
項目を設定する
次に項目を設定するためのSetTexts関数を示します。for文のなかで追加する要素のX座標を計算します。そして渡されたwidthsから各要素の幅を取得して設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
class GanttChart{ SetTexts(texts, widths, yPos){ let left = 0; let newElements = []; // ここで要素が4つ追加されるがそのなかで一番高さが高いものを記憶しておく let newElementMaxHeight = 0; for(let i=0; i<4; i++){ let newElement = document.createElement('div'); newElement.innerHTML = texts[i]; newElement.style.position = 'absolute'; newElement.style.width = (widths[i]) + 'px'; newElement.style.top = yPos + 'px'; newElement.style.left = left + 'px'; newElement.style.fontSize = '12px'; if(i!=3) newElement.style.textAlign = 'left'; else newElement.style.textAlign = 'center'; this.MainElement.appendChild(newElement); newElements.push(newElement); newElementMaxHeight = newElementMaxHeight < newElement.offsetHeight ? newElement.offsetHeight : newElementMaxHeight; left += widths[i]; } // 実際に描画するときは一番高いものに20を追加した高さでそろえる newElementMaxHeight += 20; for(let i=0; i<newElements.length; i++){ let newElement = newElements[i]; // 高さはnewElementMaxHeightにする newElement.style.height = newElementMaxHeight + 'px'; // 文字列を中央(Y方向だけ)にする this.MoveTextCellMiddle(newElement); // 各要素の境界線も描画する newElement.style.borderRight = "solid 1px"; newElement.style.borderRightColor = '#cccccc'; if(i!= 0 || texts[0] != ''){ newElement.style.borderTop = "solid 1px"; newElement.style.borderTopColor = '#cccccc'; } } // 追加された要素の高さを返す return newElementMaxHeight; } } |
文字を中央寄せにする関数を示します。
positionにabsoluteが設定されているとうまくいかないので別の要素を追加してそのなかでdisplayをtable-cellに設定して文字列を移し替えることで文字が中央寄せになったようにみせかけます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class GanttChart{ MoveTextCellMiddle(element){ let innerElement = document.createElement('div'); innerElement.innerHTML = element.innerHTML; innerElement.style.width = element.style.width; innerElement.style.height = element.style.height; innerElement.style.fontSize = element.style.fontSize; innerElement.style.display = "table-cell"; innerElement.style.verticalAlign = "middle"; innerElement.style.paddingLeft = 5 + 'px'; innerElement.style.paddingRight = 5 + 'px'; element.innerHTML = ''; element.appendChild(innerElement); } } |
バーを描画する
バーを描画するための関数 DrawBand関数を示します。
開始日と終了日からバーをどこからどこまで描画すべきかを調べます。
startDayとendDayは日数的に何日違うのかを調べます。ここでは経過時間をミリ秒で取得して引き算して1000*60*60*24で割ったものを四捨五入して日数を求めています。
またチャートの開始日と終了日のなかにタスクの開始日と終了日がおさまっているのであれば難しくないのですが、そうなっていない場合は適切な長さに切り縮めて適切な位置から描画しなければなりません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
class GanttChart{ DrawBand(startDay, endDay, yPos, newElementMaxHeight){ // 経過時間をミリ秒で取得 //var ms = to.getTime() - from.getTime(); // ミリ秒を日数に変換(四捨五入) let ms = new Date(startDay).getTime() - this.GanttChartFirstDay.getTime(); let startpos = Math.round(ms / (1000*60*60*24)); ms = new Date(endDay).getTime()-this.GanttChartFirstDay.getTime(); // 表示する最終日 let endpos = Math.round(ms / (1000*60*60*24)); let newElement1 = document.createElement('div'); newElement1.style.position = 'absolute'; newElement1.style.backgroundColor = 'rgba(255, 0, 0, 0)'; newElement1.style.top = yPos + 'px'; newElement1.style.left = this.ItemsWidth + 'px'; newElement1.style.width = this.TotalWidth - this.ItemsWidth +'px'; newElement1.style.height = '20px'; newElement1.style.borderTop = "solid 1px"; newElement1.style.borderTopColor = '#cccccc'; this.MainElement.appendChild(newElement1); let newElement2 = document.createElement('div'); newElement2.style.position = 'absolute'; newElement2.style.height = '20px'; newElement2.style.backgroundColor = 'rgba(0, 0, 255, 0.9)'; newElement2.style.top = yPos + (newElementMaxHeight - 20)/2 + 'px'; newElement2.style.fontSize = '12px'; newElement2.style.textAlign = 'left'; // バーはどこからどこまで描画すべきか? if(endpos < startpos){ // endpos < startposの場合は引数が不正なのでその旨表示する newElement2.style.backgroundColor = 'rgba(255, 0, 0, 0.9)'; newElement2.style.left = this.ItemsWidth + 'px'; newElement2.style.width = this.DayWidth + this.DayWidth * 5 +'px'; newElement2.innerHTML = ' 設定がおかしい'; newElement2.style.color = '#ffffff'; newElement2.style.textAlign = 'center'; } else if(startpos < 0){ if(endpos < 0){ // チャートの開始日よりもタスクの開始日と終了日の双方が前のときはバーは非表示 newElement2.style.width = 0 +'px'; } else if(endpos < this.DayCount){ // チャートの開始日よりもタスクの開始日が前で // 終了日がチャートの開始日と終了日の中間にある newElement2.style.left = this.ItemsWidth + 'px'; newElement2.style.width = (this.DayWidth + 1) * (endpos +1) +'px'; } else{ // チャートの開始日よりもタスクの開始日が前で // 終了日がチャートの終了日よりもあとにある newElement2.style.left = this.ItemsWidth + 'px'; newElement2.style.width = (this.DayWidth + 1) * (this.DayCount) +'px'; } } else if(startpos >= 0 && startpos <= this.DayCount){ if(endpos >= 0 && endpos < this.DayCount){ // タスクの開始日がチャートの開始日と終了日のあいだで // 終了日もチャートの開始日と終了日の中間にある newElement2.style.left = this.ItemsWidth + (this.DayWidth + 1) * startpos + 'px'; newElement2.style.width = (this.DayWidth + 1) * (endpos - startpos +1) +'px'; } else{ // タスクの開始日がチャートの開始日と終了日のあいだで // 終了日はチャートの終了日のあとにある newElement2.style.left = this.ItemsWidth + (this.DayWidth + 1) * startpos + 'px'; newElement2.style.width = (this.DayWidth + 1) * (this.DayCount - startpos) +'px'; } } // 上記以外はタスクの開始日がチャートの終了日よりもあとである場合だが // この場合はバーは非表示とする this.MainElement.appendChild(newElement2); } } |