元々の経緯としては、JavaScriptライブラリである『FullCalender.js』を使おうと思いましたが、ガントチャートは有料ライセンスを取得しなければならないため、自作することにしました。
もちろんそれ以外にも『Frappe Gant』や『jquery.ganttView』なども存在はしますが、自分で思うようにカスタマイズするためには学習コストがかかるため断念しました。
また、今回はバックエンドでLaravelを使用しています。
ガントチャート完成イメージ

ガントチャート作成
HTML
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ガントチャートテスト</title>
<link rel="stylesheet" href="{{ asset('css/chartList.css') }}">
<meta name="csrf-token" content="{{ csrf_token() }}">
</head>
<body>
<div class="main">
<div class="row_height">
</div>
<div class="row_width">
<div class="row_width_content">9:00</div>
<div class="row_width_content">11:00</div>
<div class="row_width_content">13:00</div>
<div class="row_width_content">17:00</div>
<div class="row_width_content">19:00</div>
<div class="row_width_content">21:00</div>
<div class="row_width_content">23:00</div>
</div>
<div class="content">
</div>
<div class="back">
<p>前日</p>
</div>
<div class="next">
<p>翌日</p>
</div>
</div>
<script>
const data = @json($works);
</script>
<script src="{{ asset('js/chartList.js') }}"></script>
</body>
</html>
“row_height”がイメージ図の縦の部分(名簿の一覧)
“row_width”がイメージ図の縦の部分(時間帯一覧)
“content”が動的になる部分です。
CSSにて全てabsoluteプロパティをつけています。
scriptタグで@json()というディレクティブを使用することによりLaravelからのデータベースの情報を受け取る設定にしています。
CSS
※全文は載せずに必要な部分だけ載せます
.row_height_content {
width: 100%;
height: 40px;
border-bottom: 2px solid #333;
display: flex;
align-items: center;
justify-content: center;
}
.box {
width: 7.1%;
height: 100%;
}先ほどのHTMLの状態では中身を指定していませんでしたが、
後述するJavaScriptにおいて”createElement“と”appendChild“というメゾットを使用して新たにデータベースから条件に該当するもののみ、親にクラスを付け加えます。
なお、”box”のwidthプロパティについてですが、これらは単純に9:00 ~ 25:00の14時間を1時間刻みに割った値です(なので目盛りを振っていないと少しだけズレます)。
JavaScript
JSONデータの改ざん
// 現在時刻
let now = new Date();
let year = now.getFullYear();
let month = now.getMonth() + 1;
let day = now.getDate();
function createNameArray() {
// データの数字(文字列)を数値に変換した
const arrayInt = data.map(item => item.date.split("-").map(Number));
const data2 = [];
for (let i = 0; i < data.length; i++) {
if (year === arrayInt[i][0] && month === arrayInt[i][1] && day === arrayInt[i][2]) {
data2.push(data[i]);
}
}
return data2;
}なお、渡されているJSONのdataは以下のようになっています。
[
{
"name": "大分",
"start_time": "11:00:00",
"end_time": "15:00:00",
"date": "2026-05-22",
"user_id": 3,
"id": 1,
"status": "approved"
},
]今回、「start_time」と「end_time」の時刻が文字列として渡されているため、splitとmapメゾットを用いて年・月・日ごとに分けて数値化しています。
また、日付を切り替えてもその日付のものが表示されるように、新たな配列としてdata2を用意し、合致するのもをpushメゾットで入れていきます。
なお、toLocaleDateStringメゾットで現在時刻を取得することも可能ですが、これも文字列であるため上記のコードの方が冗長にならずに済みのです。
この関数によって現在日時に関するJSONの絞り込みが完了しました。
縦列の名簿リストを生成する関数
// 縦列の名簿リストを生成する関数
// dateの長さは人数分
// 順番は子から親に連結させていくイメージ
function nameList() {
const data2 = createNameArray();
for (let i = 0; i < data2.length; i++) {
// 子コード
/// pタグを生成する
/// pタグのテキストをデータからとってくる
const p = document.createElement('p');
p.textContent = data2[i].name;
// 親コード
/// pタグの親であるdivタグを生成する
/// 親にクラス名をつける
const createBox = document.createElement('div');
createBox.classList.add('row_height_content');
// 子を親につける
createBox.appendChild(p);
// 親を祖につける
document.querySelector('.row_height').appendChild(createBox);
}
}先ほどの関数を使用して返り値にて新たな配列を取得します。
まずは子要素であるpタグを生成し、textContentメゾットで名前の要素を指定します。
次に親要素のdivタグを生成し、classList.addメゾットでクラスを指定します。
最後にappendChildメゾットで親にpタグをつけ、さらに親のdivタグをその親のタグにつけます。
これら全ての動作をまとめて関数としてのちに呼び出すのです。
なお、”innerHTML”メゾットも使用はできますが、こちらの方がDOM操作が明確なのに加え、セキュリティ的にも現在はこちらの方が好まれています。
横列のタイムシートを生成する関数
// 横列のタイムシートを生成する関数
function content() {
const data2 = createNameArray();
for (let j = 0; j < data2.length; j++) {
const createChartbox = document.createElement('div');
createChartbox.classList.add('content_1');
document.querySelector('.content').appendChild(createChartbox);
for (let i = 0; i < 14; i++) {
// 例 12:00 なら 12
const startHour = Number(data2[j].start_time.split(':')[0]);
const endHour = Number(data2[j].end_time.split(':')[0]);
// 例 12:00 なら 3コマ
const startIndex = startHour - 9;
const endIndex = endHour - 9;
// 範囲内にて色を分けるため別のクラスをつけるための条件
const active = i >= startIndex && i < endIndex;
const newDiv = document.createElement("div");
newDiv.classList.add('box');
if (active && data2[j].status === 'pending') newDiv.classList.add('act_pending');
if (active && data2[j].status === 'approved') newDiv.classList.add('act_approved');
if (active) newDiv.dataset.userId = data2[j].user_id;
if (active) newDiv.dataset.id = data2[j].id;
createChartbox.appendChild(newDiv);
}
}
}新たに生成した配列の長さの数だけ横長のタイムリストを先に生成します。
今回の計算は1時間が単位としているため、時刻の左側(12:00なら12)を、9時を始点とするため値から-9引いた値を指定しています。
承認されていれば”act_pending'”、申請中の場合では”act_approved”というクラスをつけ、色わけができるようにしているのです。

