初めてのガントチャート実装はSVAR React ganttが分かりやすい!

はじめに

 こんにちは。社内向け不具合報告アプリの機能を拡充させるべく、ガントチャート表示機能を実装しました。それに伴い使用したガントチャートや実装したことを簡単にご紹介します。


背景・課題

  • なぜ SVAR を選んだか
    • 無料版があり、かつ公式サイトに実装手順が(個人的に)わかりやすくまとまっていた。
      (今回の新規実装は私を中心で行ったので、使用ライブラリは私の一存で決めております笑)
       ※ 公式ガイド  (実装がコードベースで辞書のようにまとまっており非常に分かりやすかったです。英語のサイトですがGoogle翻訳を使えば簡単に理解できます。)
  • なぜブラウザ用ガントチャートを導入したか
    • 我々エンジニアが所属しているデザイン・システム室の業務内容を全社員に周知できる場を設け、業務の透明化を測る。
    • シーズン業務や中長期案件などの理解をいただき、より効率的な部署間の業務連携体制を作る。
    • Gitlab をFreeプランで運用している。(ガントチャート機能が使えない)
  • なぜ PRO版 ではなく無料版か
    • 無料版で提供されているライブラリとコンポーネントだけでも、ガントチャートの基盤は十分に作れる。
    • 無料版では提供されていない機能はオプション機能が多く、公式コンポーネントを使わずとも手動で追加できると判断。(後述)

全体の作り方

 今回は編集機能を実装するのではなく、GitLab 上の予定を社内向けに見せることを優先しています。

 ざっくりした流れは次のとおりです。

①GitLab の Group Issues API から Issue を取得する
②サーバー側でマイルストーン単位にデータを整える
③SVAR Gantt が読める tasks 形式に変換する
④クライアントで read-only のガントとして描画する

 ポイントは3つあります。

  • Issue に埋め込まれている milestone 情報から配下イシューの構造を組み立てる
    • API の呼び出しを Issue 取得に寄せた方が、ガントチャートに必要な「開始日」「期日」が取れるので運用上シンプルでした。マイルストーンとしてグループ化したい場合はイシューが紐づいているマイルストーンがあれば、同一のものを括れるように処理しています。
// Issue に埋め込まれた milestone.id で同じものを括る
for (const raw of issues) {
  const embed = raw.milestone;
  if (!embed?.id) continue;

  let ms = msById.get(embed.id);
  if (!ms) {
    ms = { id: embed.id, title: embed.title, issues: [] };
    msById.set(embed.id, ms);
  }
  ms.issues.push(normalizeIssue(raw)); // start_date / due_date もここで持つ
}
  • Gantt 上では編集しない
    • 期限の変更は GitLab 側で行い、アプリはあくまで「見る」ための画面にしています。そのため SVAR の readonly モードで表示しています。
<Gantt tasks={tasks} readonly />
  •  Next.js との組み合わせではSSR ではなく dynamic import
    • ガント部分だけクライアント読み込みにしています。SVAR Gantt はブラウザ前提のコンポーネントなので、 遅延読み込みするのが良いと判断しました。そもそもSSRに対応していないライブラリをそのままサーバー描画すると、エラーを吐く可能性が高いです。
const ScheduleGantt = dynamic(
  () => import('./ScheduleGantt').then((m) => m.ScheduleGantt),
  { ssr: false },
);

無料版で PRO 相当をどう寄せたか

 実際にどのように実装したのか簡単に紹介しようと思いますが、関わる行全てを紹介すると見にくくなってしまうと思うので根幹箇所だけピックアップして紹介します。

 全体像は下の全体像イメージにスクリーンショットを貼っているのでそちらの画像も参考にしてください。

1.マイルストーンの進捗バー

 マイルストーン行に、配下 Issue の完了状況を見せたかったための実装です。SVAR の PRO 版には、サマリタスクの進捗を子タスクから自動計算する機能がありますが、無料版では自分で計算して渡す必要があります。

 一部分の抜粋ですが、マイルストーンの配下 Issue のうち closed の割合を求め、マイルストーン行の progress にセットしています。

const closeProgress = milestoneCloseProgressPercent(ms.issues);

out.push({
  id: ms.id,
  type: 'summary',
  text: ms.title,
  progress: closeProgress,  // 完了数 / 全体数 で進捗を出すようにしました。
});

 見た目としては、マイルストーン行だけに進捗バーが出るように CSS で調整しています。

.wx-bar:not(.wx-summary) .wx-progress-percent { display: none !important; }

 PRO の自動集計を「データを渡す前に計算する」ことで代替できた、比較的楽なパターンです。


2. クリック式ツールチップ

 SVAR には公式のツールチップがありますが、ホバー式のツールチップしか存在せず、1ミリカーソルを動かしただけでホバーが消えてしまったのが非常に使いずらいと感じました。
特に、ノートPCのタッチパッドでの操作を想定すると、クリックの方が安定します。

 そこで、バーをクリックしたときだけ詳細を出す自前 UI にしました。SVAR がバーに付ける data-tooltip-id でタスクを特定し、担当者・状態・開始日・期限・マイルストーンのクローズ率など、GitLab 由来の情報を表示しています。

const hit = findBarFromTarget(event.target); // data-tooltip-id を辿る
const task = api?.getTask(hit.id);
setOpen({ task, top, left }); // クリックで開閉

 Gantt を自前コンポーネントでラップし、onPointerDownCapture でバークリックを拾っています。タスク取得には init={setGanttApi} で渡した API(api.getTask)を使っています。

<ScheduleGanttTooltip api={ganttApi} tasks={tasks}>
  <Gantt init={setGanttApi} tasks={tasks} readonly />
</ScheduleGanttTooltip>

 無料版に tooltip はあったのですが、このように基盤だけ拝借して使いにくい部分は自分でカスタマイズするという使い方もできるので、楽チンです。


3. PDF 出力

 会議や共有用に、画面の予定表を PDF 化したい要件がありました。PRO 版には PDF / PNG エクスポートがありますが、今回は window.print() で代替しました。

 そのまま印刷すると、画面用ガントはスクロール枠内に収まっているため、途中で切れてしまいます。そこで、印刷専用にもう1本ガントを描画し、表示行数ぶんの高さを確保してから印刷する方式にしています。

 また、印刷ボタン押下後、requestAnimationFrame で1〜2フレーム待ってから window.print() しています。高さが確定する前に印刷すると、左グリッドやバーが切れることがあったためです。

 印刷期間が長すぎるとレイアウトが崩れやすいので、表示日数に上限を設けるようにしています。今も最適化中ですが、表示したい期間(=印刷したい期間)を絞ることで、長い期間を無理に1枚に載せない運用にすれば、実用レベルまで持っていけると考えています。

縦方向(行数)は複数ページにまたがる想定で作成しました。

const height = HEADER_HEIGHT + tasks.length * ROW_HEIGHT;
{printPending && (
<div className="schedule-gantt-print-root" style={{ left: -99999 }}>
<Gantt tasks={tasks} hostHeightPx={height} />
</div>
)}
window.print(); // @media print で print-root だけ表示

 ここに関しては、印刷機能の実装が必要であることも、無料版には印刷機能がないことも分かってはいましたが、この程度であれば自作できるレベルです。


全体像イメージ

 最終的にこのような形で実装しました。(一部加工しております)

 ブラウザの幅に合わせた表示、期間の設定、バーのクリックでのツールチップの表示など
ほしい機能は全て無料で簡単に作成することができました。

 ちなみに、左のIssue名が並んでいる箇所(黒塗りしている部分)はリンクになっております。
社員からデザイン・システム室へ改修の依頼をする機能が既存であるのですが、その依頼済み一覧の詳細ページへのリンクとなっているので、どんな依頼だったかの確認などへの導線も設けられております。


まとめ

 初めてのガント実装でも、SVAR React Gantt の無料版は基盤として十分使えました。進捗集計・ツールチップ・PDF など PRO 寄りの機能は自前で補う必要がありましたが、編集しない「見える化」用途なら無料版でも実用レベルまで持っていけます。

 Reactは経験が薄い言語だったのですが、便利機能が揃っているおかげでベース作りはライブラリにお任せして、細かい調整を行なっていくと言った運用ができることが分かり、自分自身のReactへのハードルが下がったように感じます。

 今後も少しずつこの社内共有機能を充実させていき、各部署との連携強化に繋げていこうと考えております。


さいごに

現在デザイン・システム室では、新しいメンバーを募集しています。少しでも興味を持たれた方は、ぜひご応募ください✨💻
皆様からのご応募、心よりお待ちしております。