5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

個人開発でガントチャートライブラリを作ってみた

Posted at

react-gantt-flow

個人開発でガントチャートライブラリを作成しており、先日ベータ版をリリースしました🎉
良きタイミングでアウトプットしたいな〜と思っていたので、いくつか記事を分けてまとめていければと思います

どんな機能があるの?

現状まだまだ機能不足ですが、最低限の機能は用意できました👏
詳細は前述の Storybook を参照ください🙇

機能 ひとことで
ドラッグ & リサイズ タスクの開始・終了日をマウス操作で更新
進捗バー(スライダー) 0 ~ 100 % をリアルタイム編集
依存矢印 タスク間を自動で S 字曲線で接続
Today ライン / 計画↔実績差分 boolean でオン・オフ切り替え
Type-safe API 全プロップに厳密な TS 型を付与

なんでガントチャート?

今回ガントチャートを作成した理由は、"今後作りたいシステムの 1 機能としてガントチャートの機能が欲しかったから" というシンプルな理由です

既存の OSS (Frappe Gantt など) でも正直よかったのですが、私が調べた限り、スケジュールと現在進捗との乖離が一目でわかる機能(いわゆる稲妻線表示機能)を持ったライブラリが無かった為、「じゃあ作ってみよう」と作成を始めました🏃‍♀️

技術選定

技術選定を以下にまとめます
至ってシンプルな構成です

項目 選定内容
UI React 19 + TypeScript
チャート描画 SVG
日付計算 date-fns
ビルド Vite
パッケージマネージャー pnpm
品質 Vitest / React Testing Library / Chromatic(VRT)
静的解析・フォーマッター Biome
ドキュメンテーション Storybook
CI/CD GitHub Actions

実装方針

react-gantt-flow はチャート部分の描画は全て svg で作成しています

なぜ全て svg なのか

以下のポイントと比較表の内容に基づき svg での実装を選びました
あと、自分で svg をろくに書いたことがなかったので、この際に学びたいなという個人的な気持ちもありました👀

ポイント どう効くか
解像度フリー/ズームに強い ・ガントは時間軸を拡大縮小する UI
・SVG はベクタなので 1 px 単位のスナップや Hi-DPI でも線がにじまない
・Canvas だと再描画が必須、DOM‐div だとサブピクセル誤差が残る
DOM ノードを極小化できる <svg> 内にバー・グリッド・矢印をすべて <g> でまとめれば “1 行 ≒ 数ノード”
・HTML で <div> を行列配置すると千~万単位のノードが増えレイアウト/スタイル計算が激重
1 レイヤ描画で GPU 合成が効く ・まとまった SVG はブラウザで 単一のコンポジットレイヤ になる。
・水平方向のスクロールや transform はレイヤを動かすだけで再ペイント不要の為、60 fps を維持しやすい
ベジェ曲線・パスが 標準装備 ・依存矢印の S 字やプロジェクト線など、Path API ひとつで表現可能
・Canvas も描けるが毎フレーム座標計算と再描画が必要、HTML だと不可能

svg / Canvas / Html + CSS 比較表

特性 SVG Canvas HTML + CSS
解像度依存 なし あり なし (だがサブピクセル誤差)
ノード数 1 枚
インタラクション DOM イベント 手動ヒットテスト DOM イベント
描画コスト 初回のみ 逐次 レイアウト+ペイント

蛇足

SVG のチュートリアルページです
ここの内容を一通り学習するだけでも、かなり勉強になりました📝

ブランチ戦略

作成したライブラリを公開するにあたり、以下のブランチ戦略を採用しました
今回初めてライブラリ開発をしてみて、通常のシステム開発とブランチ戦略が異なる部分に難しさを感じてます😖

こちらのモデルでは以下の 3 つの特徴を意識しています

  • main でメジャーバージョン向けに高速開発
  • 複数の安定ブランチを長期保守
  • 緊急修正をタイムリーに横展開

スクリーンショット 2025-06-14 22.01.11.png

メリットデメリットについては以下の点が挙げられると思っています
正直デメリットのつらみから抜け出せないので、次回はもっとシンプル構成に改善したいです
ただ、考慮すべき観点は一通り把握できたので、とても良い機会でした🎉

メリット☺️

観点 効果
LTS と最新を両立 release/1.x を残すことで「現場はまだ v1 だけどバグは直したい」を実現。
高速なホットフィックス 修正は最短距離で影響版に届け、あとから cherry-pick で他系統へ展開でき、ダウンタイムを最小化
明確な責務分割 - main = 未来
- release/x = 安定
- feature/* = 新機能
- fix/* = 緊急修正
タグ運用しやすい リリース物は必ず release/* でタグ付け
CI/CD で “そのタグだけ npm publish” といった制御が容易

デメリット😭

課題 具体的な痛点
Cherry-pick 地獄 手動漏れ・コンフリクト対応コストが高い。
GitHub Actions で “fix マージ時に自動 PR 作成” を仕込むと負荷を下げられる
CI コスト増 ブランチが増えるほどビルドマトリクスが膨らみ、ワークフローを最適化しないとランナー課金が跳ねる可能性あり
学習コスト  “いつ、どのブランチを切るか” が難解

CI/CD

続いて CI/CD のワークフローと設定ファイルについて、抜粋してご紹介します
ライブラリ公開においては以下の 3 種を活用しています

pr-auto-labeler

PR を作成した際に、ブランチの prefix に応じて、自動的にラベルを付与してくれる workflow です
後述する release note に必要の為、用意しています

pr-auto-labeler.yml


name: PR auto Labeler

on:
  pull_request:
    types: [opened]

jobs:
  pr-labeler:
    runs-on: ubuntu-latest
    steps:
      - uses: TimonVS/pr-labeler-action@v3
        with:
          configuration-path: .github/pr-labeler-settings.yml
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
pr-labeler-settings.yml


chore: ["chore/*", "renovate/*"]
docs: "docs/*"
feature: ["feature/*", "feat/*", "topic/*", "experiment/*"]
fix: ["fix/*", "hotfix/*", "backport/*"]
refactor: "refactor/*"

release note

release note の自動作成は package-publish 内で実施するのですが、体裁を整える為、以下の設定ファイルを用意しています

release.yml


changelog:
  categories:
    - title: New Features 🎉
      labels:
        - feature
    - title: Bug Fixes 🐛
      labels:
        - fix
    - title: Maintenance & Chores 🔧
      labels:
        - chore
    - title: Code Refactoring ✨
      labels:
        - refactor
    - title: Other Changes
      labels:
        - "*"

package-publish

publish 用の workflow では特殊なことはしていないです
dispatch から Semantic Versioning でリリースできるよう設定を追加してます

Create GitHub Release (Automatic Notes) の step で、release.yml 通りの体裁で PR が自動的に分類されリリースノートが作成されます🎉

public-package.yml


name: Publish Packages

on:
  workflow_dispatch:
    inputs:
      release_type:
        description: "Select release type"
        required: true
        type: choice
        default: "patch"
        options:
          - major
          - minor
          - patch
          - premajor
          - prerelease
      prerelease_id:
        description: "Select prerelease id (used only if release_type is prerelease)"
        required: false
        type: choice
        default: "alpha"
        options:
          - alpha
          - beta
          - rc

jobs:
  publish:
    runs-on: ubuntu-latest

    #...省略

    steps:
      # ブランチが release/ で始まることを確認し、release/ 以外の場合はエラーを出す
      - name: Verify release branch
        if: ${{ !startsWith(github.ref, 'refs/heads/release/') }}
        run: |
          echo "This workflow can only be run on release branches."
          exit 1

      # ~~ チェックアウト やら ビルド やら ~~

      # release branch 用にバージョンを更新
      - name: Bump version for release branch
        id: bump_version
        run: |
          if [ "${{ inputs.release_type }}" = "premajor" ] || [ "${{ inputs.release_type }}" = "prerelease" ]; then
            NEW_VERSION=$(npm version ${{ inputs.release_type }} --preid "${{ inputs.prerelease_id }}" -m "chore: Bump version to %s")
          else
            NEW_VERSION=$(npm version ${{ inputs.release_type }} -m "chore: Bump version to %s")
          fi

          echo "NEW_VERSION=${NEW_VERSION}" >> "$GITHUB_OUTPUT"

      # コミットとタグをプッシュ
      - name: Push commit and tags
        run: git push --follow-tags

      # 公開
      - name: Publish 🎁
        run: pnpm publish --provenance --access public --publish-branch "${GITHUB_REF#refs/heads/}" --no-git-checks
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      # ~~ main へのバージョン同期 ~~

      # release note の作成
      # release note の設定は release.yml を参照
      - name: Create GitHub Release (Automatic Notes)
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const tagName = process.env.NEW_VERSION;

            const response = await github.rest.repos.createRelease({
              owner: context.repo.owner,
              repo: context.repo.repo,
              tag_name: tagName,
              name: tagName,
              generate_release_notes: true,
              draft: false,
              prerelease: (tagName.includes('-')),
            });

            core.info(`Created release: ${response.data.html_url}`);
            core.setOutput("release_id", response.data.id);
            core.setOutput("release_url", response.data.html_url);
        env:
          NEW_VERSION: ${{ steps.bump_version.outputs.NEW_VERSION }}

ロードマップ

今後は以下の機能追加と改善に取り組む想定です

優先度 予定
High アクセシビリティ対応 (キーボード操作 / ARIA)
High 親子タスク表示 (グルーピング)
Mid TaskBar API: onClick / onContextMenu など
Mid テーマカスタマイズ & ダークモード
Low UI の微調整

まとめ

react-gantt-flow は「軽量 × 稲妻線 × 型安全」が売りの OSS ガントチャートです
今後は A11y & グルーピングを強化予定となります😉

ライブラリ開発はシステム開発とは違った面白さがありとても魅力的です

次回記事では一番苦戦した dependenciesArrow の実装について深掘り or ライブラリ作成の為の Vite 設定 のどちらかです!お楽しみに! 🙌

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?