プロジェクトの工程表——ガントチャートを作りたいだけなのに、いざ探すと選択肢が極端です。
Excel で手作りするとバーの位置合わせが地獄。専用ツールは高機能な代わりに、アカウント登録して、チームを作って、有料プランに誘導されて……「1 枚のガントチャートを PNG で欲しいだけ」には過剰です。
GanttPad は、タスクと開始日・終了日を入れると即座にガントチャートを描く、それだけのツールです。登録なし、その場で PNG / SVG に書き出し。日付をバーの位置に変換する部分を自前で実装しました。
#🎯 「表を入れたら図になる」に絞る
ガントチャート作成の面倒さは、ほぼ 「日付とバーの位置合わせ」 に集約されます。3 月 10 日開始・3 月 20 日終了のタスクを、横軸のどこからどこまでのバーにするか。タスクが増えるほど手作業では合わなくなる。
GanttPad はここだけを自動化しました。タスク名・開始日・終了日を入力すると、あとは全部計算で位置が決まります。進捗の塗り、今日の線、目盛りの自動調整——「表を入れたら図が出る」以上のことはしません。
#🏗️ 日付を「オフセット」に変換する
ガントチャートの本質は、それぞれのタスクを「全体の開始日から何日目に、何日間」のバーに変換することです。GanttPad の computeLayout がこれをやっています。
まず、全タスクの中で最も早い開始日と最も遅い終了日を見つけて、チャート全体の期間を決めます。
const rangeStart = Math.min(...valids.map((p) => p.s))
const rangeEnd = Math.max(...valids.map((p) => p.e))
const totalDays = dayDiff(rangeStart, rangeEnd) + 1そのうえで、各タスクを「全体開始日からのオフセット(何日目に始まるか)」と「日数(何日間続くか)」に変換します。
const bars: Bar[] = parsed.map((p) => {
if (!p.valid) return { task: p.t, offset: 0, duration: 1, valid: false }
return {
task: p.t,
offset: dayDiff(rangeStart, p.s as number), // 何日目から
duration: dayDiff(p.s as number, p.e as number) + 1, // 何日間
valid: true,
}
})これで、描画側は offset × 1日の幅 の位置に、duration × 1日の幅 の長さのバーを置くだけになります。日付という扱いにくいものを、一度「整数の日数」に落としてしまえば、あとはただの掛け算です。
#日付は UTC で扱う
地味に重要なのが、日付をすべて UTC で計算していることです。
export function todayUTC: number { ... }
function fmtMD(t: number): string {
const d = new Date(t)
return `${d.getUTCMonth + 1}/${d.getUTCDate}`
}ローカルタイムで日付を引き算すると、夏時間やタイムゾーンの境目で「1 日ずれる」バグが出ます。工程表で 1 日ずれると信用を失う。日付だけを扱うなら、時刻・タイムゾーンの要素を持たない UTC で通すのが安全でした。
#📏 目盛りは期間の長さで自動的に切り替える
2 週間のプロジェクトと 2 年のプロジェクトを、同じ目盛りで描くことはできません。前者は 1 日ずつ、後者は月ごとに刻むべきです。
GanttPad は、全体の日数に応じて 1 日あたりの幅と目盛りの粒度を自動で変えます。
const dayWidth = totalDays <= 21 ? 26 : totalDays <= 70 ? 12 : totalDays <= 210 ? 6 : 3
const dayMode = totalDays <= 45- 短い(〜45 日)→ 日単位の目盛り、週末に色を敷く
- 中くらい(〜210 日)→ 週単位(月曜揃え)の目盛り
- 長い(それ以上)→ 月単位の目盛り
日単位モードでは、土日を検出して薄く塗ります。
if (dayMode) {
for (let i = 0; i < totalDays; i++) {
const t = rangeStart + i * DAY
const dow = new Date(t).getUTCDay
if (dow === 0 || dow === 6) weekends.push(i)
const major = new Date(t).getUTCDate === 1 || i === 0
ticks.push({ offset: i, label: fmtMD(t), major })
}
}週末が見えると「実働日」が直感的に分かるので、短期のスケジュールでは効きます。逆に半年を超える表で週末を全部塗ると真っ黒になるので、日数が増えたらこの装飾は自動で消えます。表示の密度は、データ量に応じて自分で調整する——これをやらないと、短くても長くても見づらいチャートになります。
#📍 「今日」の線
進行中のプロジェクトで一番見たいのは「今、予定に対してどこにいるか」です。GanttPad は今日の位置に縦線を引きます。
const today = todayUTC
const todayOffset = today >= rangeStart && today <= rangeEnd ? dayDiff(rangeStart, today) : nullポイントは、今日がチャートの期間内にあるときだけ線を出すこと(null なら描かない)。過去のプロジェクトや未来の計画では今日の線は意味がないので、範囲外なら黙って消えます。この縦線と、各バーの進捗塗りを見比べると、「予定より進んでいる/遅れている」が一目で分かります。
#😓 苦労したところ
#不正な入力で全体を壊さない
ユーザーは日付を空欄にしたり、終了日を開始日より前にしたりします。1 つのタスクが壊れているせいでチャート全体が描けなくなると、使い物になりません。
そこで、各タスクを個別に検証し、有効なものだけで全体の期間を決めるようにしました。
const valids = parsed.filter((p) => p.valid) as Array<...>
if (valids.length === 0) return null不正なタスクは valid: false のバーとして持っておき、描画側でグレー表示などにして「これは日付がおかしい」と示す。有効なタスクが 1 つもないときだけ、チャート自体を出さない。1 行の入力ミスで全部が消える、という事態を避けています。
#色を配列で回す
タスクごとに色を変えたいけれど、ユーザーに毎回色を選ばせるのは面倒です。あらかじめ見分けやすい 8 色を用意して、順番に割り当てています。
export const BAR_COLORS = ["#2d6cdf", "#16a085", "#e08a1e", "#c0392b", "#8e44ad", "#0e7c86", "#d6336c", "#5c6bc0"]隣り合うバーが似た色にならないよう、色相を散らした並びにしてあります。8 色を超えたら先頭に戻りますが、隣接タスクが同色になる確率は低く、実用上は困りません。
#SVG で作れば PNG も SVG も出せる
チャートは SVG で描いています。SVG なら、そのまま「SVG で保存」できるうえ、canvas に一度描いてから「PNG で保存」もできる。ベクターで持っておけば両方の出力形式に対応できるので、描画のソースを SVG にしたのは自然な選択でした。拡大しても劣化しないので、資料に大きく貼るときにも困りません。
#🔭 今後の拡張
- 依存関係(矢印) — 「タスク A が終わってから B」を線で結ぶ
- マイルストーン — 特定の日に菱形のマーカーを置く
- 担当者ごとの色分け・グループ化
- CSV の読み込み/書き出し — 表計算ソフトとの行き来
- クリティカルパスの強調 — 全体の期間を決めている連鎖を色で示す
依存関係まで入ると本格的な工程管理に近づきますが、そこは専用ツールの領域でもある。GanttPad は「表を図にする」手軽さを保つ範囲で機能を選ぶつもりです。
#💡 このサービスから言えること
扱いにくいデータは、一度シンプルな形に落とすと急に楽になる。 日付はタイムゾーンも桁もあって扱いにくいものですが、「全体開始日からの日数(整数)」に変換してしまえば、あとの計算は全部ただの掛け算と足し算です。ガントチャートというと難しそうに見えて、核心は「日付 → オフセット」の一手でした。
もうひとつは、表示の密度を自分で調整するという発想です。同じ描画コードでも、2 週間と 2 年では見せ方を変えないと使えない。データ量に応じて目盛りの粒度や装飾を切り替えるのは、データ可視化ツールを作るときに必ず出てくる判断で、ここを自動化できると「どんな入力でも見やすい」ツールになります。
そして、ガントチャートのような「入力さえあれば計算だけで完結する」ものは、サーバーを持つ理由がありません。ブラウザ内で描いて書き出すだけなら、登録も課金もなしに、その場で完結できます。
[ ./next_action ]
読んだら、 GanttPad を実際に動かす。
この開発ログは GanttPad をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。