お試し 技術

JavaScriptでガンチャートを実装する

JavaScriptでなるべく依存の少ない感じで実装してみようと思い【chart.js】を使用してガンチャートを実装してみました

名称バージョン
NodeJS22.14.0
chart.js4.5.0
chartjs-adapter-date-fns4.1.0
ejs3.1.10
express5.1.0

ガンチャートの初期化

まずは、Chart.jsを使えるように初期化します。今回はガンチャートとして使用するのでそのための設定をします

// Chart.jsでガンチャートを初期化
initChart() {
    const ctx = document.getElementById('ganttChart').getContext('2d');
    
    if (this.chart) {
        this.chart.destroy();
    }
    // Y軸のラベルを作成
    const taskLabels = this.ganttData.map(task => `${task.taskName} (${task.assignee})`);
    // データセットを一つにまとめる
    const chartData = this.ganttData.map((task, index) => {
        const startDate = new Date(task.startDate);
        const endDate = new Date(task.endDate);
        
        return {
            x: [startDate, endDate],
            y: index, // インデックスを使用
            backgroundColor: this.adjustOpacity(task.color, 0.8),
            borderColor: task.color,
            borderWidth: 2,
            taskInfo: task
        };
    });
    this.chart = new Chart(ctx, {
        type: 'bar',
        data: {
            labels: taskLabels,
            datasets: [{
                label: 'タスク期間',
                data: chartData,
                backgroundColor: chartData.map(item => item.backgroundColor),
                borderColor: chartData.map(item => item.borderColor),
                borderWidth: 2,
                barThickness: 25,
                categoryPercentage: 0.8,
                barPercentage: 0.9
            }]
        },
        options: {
            responsive: true,
            maintainAspectRatio: false,
            indexAxis: 'y',
            interaction: {
                intersect: false,
                mode: 'nearest'
            },
            scales: {
                x: this.getXAxisConfig(),
                y: {
                    type: 'category',
                    labels: taskLabels,
                    title: {
                        display: true,
                        text: 'タスク',
                        font: {
                            size: 14,
                            weight: 'bold'
                        }
                    },
                    grid: {
                        color: '#f0f0f0',
                        lineWidth: 1
                    },
                    ticks: {
                        font: {
                            size: 12
                        }
                    }
                }
            },
            plugins: {
                legend: {
                    display: false
                },
                tooltip: {
                    backgroundColor: 'rgba(0,0,0,0.8)',
                    titleColor: 'white',
                    bodyColor: 'white',
                    borderColor: '#3498db',
                    borderWidth: 1,
                    callbacks: {
                        title: function(context) {
                            try {
                                const dataIndex = context[0].dataIndex;
                                const task = ganttManager.ganttData[dataIndex];
                                return task ? task.taskName : '';
                            } catch (error) {
                                console.error('Tooltip title error:', error);
                                return '';
                            }
                        },
                        label: function(context) {
                            try {
                                const dataIndex = context.dataIndex;
                                const task = ganttManager.ganttData[dataIndex];
                                if (!task) return '';
                                
                                return [
                                    `開始: ${task.startDate}`,
                                    `終了: ${task.endDate}`,
                                    `進捗: ${task.progress}%`,
                                    `担当: ${task.assignee}`
                                ];
                            } catch (error) {
                                console.error('Tooltip label error:', error);
                                return '';
                            }
                        }
                    }
                },
                zoom: {
                    limits: {
                        x: {min: 'original', max: 'original'},
                    },
                    pan: {
                        enabled: true,
                        mode: 'x',
                        modifierKey: 'ctrl',
                    },
                    zoom: {
                        wheel: {
                            enabled: true,
                        },
                        pinch: {
                            enabled: true
                        },
                        mode: 'x',
                    }
                }
            },
            onHover: (event, elements) => {
                if (event.native && event.native.target) {
                    event.native.target.style.cursor = elements.length > 0 ? 'pointer' : 'default';
                }
            },
            onClick: (event, elements) => {
                if (elements.length > 0) {
                    const dataIndex = elements[0].index;
                    const task = this.ganttData[dataIndex];
                    if (task) {
                        this.showTaskDetails(task);
                    }
                }
            }
        }
    });
}

タスクを作成する

HTML上で入力した値をexpress経由で登録します

// タスクを追加
async addTask() {
    const taskName = document.getElementById('taskName').value.trim();
    const startDate = document.getElementById('startDate').value;
    const endDate = document.getElementById('endDate').value;
    const progress = document.getElementById('progress').value;
    const assignee = document.getElementById('assignee').value.trim();
    // バリデーション
    if (!taskName || !startDate || !endDate) {
        alert('タスク名、開始日、終了日は必須です');
        return;
    }
    if (new Date(startDate) > new Date(endDate)) {
        alert('開始日は終了日より前の日付を選択してください');
        return;
    }
    try {
        this.setLoading(true);
        
        const response = await fetch('/api/tasks', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                taskName,
                startDate,
                endDate,
                progress: parseInt(progress) || 0,
                assignee: assignee || '未割当'
            })
        });
        if (response.ok) {
            const newTask = await response.json();
            this.ganttData.push(newTask);
            this.initChart();
            this.updateTaskList();
            this.clearForm();
            this.showNotification('タスクが正常に追加されました', 'success');
        } else {
            throw new Error('サーバーエラー');
        }
    } catch (error) {
        console.error('エラー:', error);
        this.showNotification('タスクの追加に失敗しました', 'error');
    } finally {
        this.setLoading(false);
    }
}

タスクの更新

こちらも対象となるタスクを編集し、そのデータをexpress経由で編集します

// タスクデータを更新
async updateTaskData(taskId, updateData) {
    try {
        this.setLoading(true);
        
        const response = await fetch(`/api/tasks/${taskId}`, {
            method: 'PUT',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(updateData)
        });
        if (response.ok) {
            const updatedTask = await response.json();
            const taskIndex = this.ganttData.findIndex(task => task.id === taskId);
            if (taskIndex !== -1) {
                this.ganttData[taskIndex] = updatedTask;
                this.initChart();
                this.updateTaskList();
                this.showNotification('タスクが更新されました', 'success');
            }
        } else {
            throw new Error('サーバーエラー');
        }
    } catch (error) {
        console.error('エラー:', error);
        this.showNotification('タスクの更新に失敗しました', 'error');
    } finally {
        this.setLoading(false);
    }
}

最後に

今回は。chart.jsを使用してガンチャートを作成しましたが、他にも色々とライブラリがあるので自身の環境にあったものを選択して使用してみてください。
また、いつものように今回使用したコードをGitHubにアップしているので参考にしてみてください

サンプルコード
BlogSampleCodeProjects/Javascript_Gantt_Chart at main · nasuton/BlogSampleCodeProjects · GitHub
BlogSampleCodeProjects/Javascript_Gantt_Chart at main · nasuton/BlogSampleCodeProjects · GitHub

Project for sample code used in the blog.(Blogで記載しているサンプルコード ...

GitHubへ

会社紹介

私が所属しているアドバンスド・ソリューション株式会社(以下、ADS)は一緒に働く仲間を募集しています

会社概要
「技術」×「知恵」=顧客課題の解決・新しい価値の創造

この方程式の実現はADSが大切にしている考えで、技術を磨き続けるgeekさと、顧客を思うloveがあってこそ実現できる世界観だと思っています
この『love & geek』の精神さえあれば、得意不得意はno problem!
技術はピカイチだけど顧客折衝はちょっと苦手。OKです。技術はまだ未熟だけど顧客と知恵を出し合って要件定義するのは大好き。OKです
凸凹な社員の集まり、色んなカラーや柄の個性が集まっているからこそ、常に新しいソリューションが生まれています

ミッション
私たちは、テクノロジーを活用し、業務や事業の生産性向上と企業進化を支援します

ホームページ
アドバンスド・ソリューション株式会社|ADS Co., Ltd.
アドバンスド・ソリューション株式会社|ADS Co., Ltd.

Microsoft 365/SharePoint/Power Platform/Azure による DX コンサル・シス ...

サイトへ移動

PR

-お試し, 技術
-,