▍AI Dev Lab
GanttPad

GanttPad ができるまで — 日付軸タイムラインのレイアウト計算

ガントチャート作成ツールの開発ログ。 開始日と終了日から全体期間を求めてバーの位置と長さを算出し、 期間に応じて日・週・月の目盛りを自動で切り替える描画の作り方を記録しています。

プロジェクトの工程表——ガントチャートを作りたいだけなのに、いざ探すと選択肢が極端です。

Excel で手作りするとバーの位置合わせが地獄。専用ツールは高機能な代わりに、アカウント登録して、チームを作って、有料プランに誘導されて……「1 枚のガントチャートを PNG で欲しいだけ」には過剰です。

GanttPad は、タスクと開始日・終了日を入れると即座にガントチャートを描く、それだけのツールです。登録なし、その場で PNG / SVG に書き出し。日付をバーの位置に変換する部分を自前で実装しました。


#🎯 「表を入れたら図になる」に絞る

ガントチャート作成の面倒さは、ほぼ 「日付とバーの位置合わせ」 に集約されます。3 月 10 日開始・3 月 20 日終了のタスクを、横軸のどこからどこまでのバーにするか。タスクが増えるほど手作業では合わなくなる。

GanttPad はここだけを自動化しました。タスク名・開始日・終了日を入力すると、あとは全部計算で位置が決まります。進捗の塗り、今日の線、目盛りの自動調整——「表を入れたら図が出る」以上のことはしません。


#🏗️ 日付を「オフセット」に変換する

ガントチャートの本質は、それぞれのタスクを「全体の開始日から何日目に、何日間」のバーに変換することです。GanttPad の computeLayout がこれをやっています。

まず、全タスクの中で最も早い開始日と最も遅い終了日を見つけて、チャート全体の期間を決めます。

ts
const rangeStart = Math.min(...valids.map((p) => p.s))
const rangeEnd = Math.max(...valids.map((p) => p.e))
const totalDays = dayDiff(rangeStart, rangeEnd) + 1

そのうえで、各タスクを「全体開始日からのオフセット(何日目に始まるか)」と「日数(何日間続くか)」に変換します。

ts
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 で計算していることです。

ts
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 日あたりの幅と目盛りの粒度を自動で変えます。

ts
const dayWidth = totalDays <= 21 ? 26 : totalDays <= 70 ? 12 : totalDays <= 210 ? 6 : 3
const dayMode = totalDays <= 45
  • 短い(〜45 日)→ 日単位の目盛り、週末に色を敷く
  • 中くらい(〜210 日)→ 週単位(月曜揃え)の目盛り
  • 長い(それ以上)→ 月単位の目盛り

日単位モードでは、土日を検出して薄く塗ります。

ts
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 は今日の位置に縦線を引きます。

ts
const today = todayUTC
const todayOffset = today >= rangeStart && today <= rangeEnd ? dayDiff(rangeStart, today) : null

ポイントは、今日がチャートの期間内にあるときだけ線を出すこと(null なら描かない)。過去のプロジェクトや未来の計画では今日の線は意味がないので、範囲外なら黙って消えます。この縦線と、各バーの進捗塗りを見比べると、「予定より進んでいる/遅れている」が一目で分かります。


#😓 苦労したところ

#不正な入力で全体を壊さない

ユーザーは日付を空欄にしたり、終了日を開始日より前にしたりします。1 つのタスクが壊れているせいでチャート全体が描けなくなると、使い物になりません。

そこで、各タスクを個別に検証し、有効なものだけで全体の期間を決めるようにしました。

ts
const valids = parsed.filter((p) => p.valid) as Array<...>
if (valids.length === 0) return null

不正なタスクは valid: false のバーとして持っておき、描画側でグレー表示などにして「これは日付がおかしい」と示す。有効なタスクが 1 つもないときだけ、チャート自体を出さない。1 行の入力ミスで全部が消える、という事態を避けています。

#色を配列で回す

タスクごとに色を変えたいけれど、ユーザーに毎回色を選ばせるのは面倒です。あらかじめ見分けやすい 8 色を用意して、順番に割り当てています。

ts
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 をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。

[ ./related_logs ]

関連する開発ログ

all logs →
ShadowTwin

立体パズルの別解を作る:同じ影と最少の積み木

ShadowTwinの開発記録。3方向の図から別々の最少配置を2つ探す仕組み、5問の独立全探索、スマホの横並び比較、保存・再編集と入力検査を解説します。実照明の計算や能力診断ではありません。

read log →
SlitCard

スリットアニメーションを作る:4コマの合成と透明原稿の設計

SlitCardの開発記録。4コマの帯を交互に合成し、すき間と黒帯で別のコマを見せる仕組みを解説。同縮尺の台紙・透明シートSVG、50mm確認線、手動プレビュー・初期非表示、再編集と実出力検査を整理します。

read log →
WindowLight

日当たりシミュレーションを作る:窓の光を床へ投影する幾何モデル

WindowLightの開発記録。太陽位置近似から窓の4隅を床へ投影し、凸多角形を部屋で切り取る設計を解説。国立天文台の16時刻との限定照合、大気差・低高度・実際の採光との違い、SVGと設定の再編集を整理します。

read log →
GridLights

電源構成の比較を作る:割合と実量を100個の電球で読み解く

電源構成比較GridLightsの開発記録。12か国の年次発電データを最大剰余法で100個の電球に変換し、割合とTWhを比較する設計を解説。欠損を0補完しない7分類、予想と実値、出典付きSVGと実データ検証を整理します。

read log →
SoundLoom

音のお絵かきを作る:実WAVから絵を測り直す設計

音のお絵かきの開発記録。描画を正弦波のPCMとWAVへ変換し、保存音声の周波数をGoertzel法で測って比較する仕組みを解説。音量・測定窓・量子化の限界と、編集・離脱時の古い結果を抑止する検証を整理します。

read log →
MazeLoom

宝探し迷路を作る:寄り道まで含めた最短ルートの探索

宝探し迷路の開発記録。通路を木として生成し、現在地と3つの宝の取得状態を組み合わせて最短ルートを探す仕組みを解説。問題コードの版固定、画面と解答付きPDFの一致、手数と印刷の検証を整理します。

read log →