Chart.js

Canvas要素を使用したレスポンシブなチャートライブラリ。8種類の基本チャート、アニメーション、インタラクション機能を提供。シンプルなAPI設計により、迅速なデータ可視化が可能。

JavaScriptデータビジュアライゼーションチャートグラフCanvasアニメーションレスポンシブ

GitHub概要

chartjs/Chart.js

Simple HTML5 Charts using the <canvas> tag

スター66,235
ウォッチ1,355
フォーク11,960
作成日:2013年3月17日
言語:JavaScript
ライセンス:MIT License

トピックス

canvaschartgraphhtml5html5-chartsjavascript

スター履歴

chartjs/Chart.js Star History
データ取得日時: 2025/7/19 02:41

フレームワーク

Chart.js

概要

Chart.jsは、HTML5 Canvasを使用してレスポンシブで美しいチャートを簡単に作成できるオープンソースのJavaScriptライブラリです。

詳細

Chart.js(チャートジェーエス)は、2013年にNick Downieによって開発されたデータビジュアライゼーション専用のJavaScriptライブラリです。HTML5 Canvasベースで高性能なレンダリングを実現し、8種類の基本チャートタイプ(線グラフ、棒グラフ、レーダーチャート、ドーナツグラフ、極座標エリアチャート、バブルチャート、散布図、混合チャート)を提供します。レスポンシブデザイン対応、滑らかなアニメーション、豊富なカスタマイズオプション、プラグインシステムによる拡張性、TypeScript完全対応などが特徴です。設定が簡単で学習コストが低く、複雑なデータビジュアライゼーションを短時間で実装できるため、Web開発者に広く採用されています。企業のダッシュボード、分析ツール、レポート機能、管理画面などで活用されており、特にReactやVue.jsなどのモダンフレームワークとの組み合わせで威力を発揮します。

メリット・デメリット

メリット

  • シンプルな API: 直感的で学習しやすい設定方法
  • レスポンシブ対応: 自動的にデバイスサイズに適応
  • 豊富なチャートタイプ: 8種類の基本チャートと多数のバリエーション
  • 滑らかなアニメーション: 美しい描画アニメーションとトランジション
  • 高いカスタマイズ性: 色、フォント、ラベル等の詳細設定が可能
  • 軽量: 比較的小さなファイルサイズで高機能
  • プラグインエコシステム: 豊富なサードパーティプラグイン
  • フレームワーク対応: React、Vue.js、Angular等との統合ライブラリ

デメリット

  • Canvas 制限: DOM操作ベースのライブラリと比較してアクセシビリティが劣る
  • 大量データの性能: 数万点以上のデータ表示時にパフォーマンス低下
  • 複雑な可視化の限界: 非定型的なビジュアライゼーションは実装困難
  • モバイル操作: タッチイベントやピンチズームの実装が限定的
  • メモリ使用量: 複数のチャートを同時表示時のメモリ消費
  • SVG非対応: ベクター形式での出力が困難

主要リンク

書き方の例

Hello World

<!DOCTYPE html>
<html>
<head>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
    <div style="width: 400px; height: 200px;">
        <canvas id="myChart"></canvas>
    </div>
    
    <script>
        // Chart.jsの基本的な使用方法
        const ctx = document.getElementById('myChart').getContext('2d');
        
        const myChart = new Chart(ctx, {
            type: 'bar',
            data: {
                labels: ['Apple', 'Banana', 'Cherry', 'Date'],
                datasets: [{
                    label: '売上数',
                    data: [12, 19, 3, 5],
                    backgroundColor: [
                        'rgba(255, 99, 132, 0.8)',
                        'rgba(54, 162, 235, 0.8)',
                        'rgba(255, 205, 86, 0.8)',
                        'rgba(75, 192, 192, 0.8)'
                    ],
                    borderColor: [
                        'rgba(255, 99, 132, 1)',
                        'rgba(54, 162, 235, 1)',
                        'rgba(255, 205, 86, 1)',
                        'rgba(75, 192, 192, 1)'
                    ],
                    borderWidth: 1
                }]
            },
            options: {
                responsive: true,
                plugins: {
                    title: {
                        display: true,
                        text: 'フルーツ売上チャート'
                    }
                },
                scales: {
                    y: {
                        beginAtZero: true
                    }
                }
            }
        });
    </script>
</body>
</html>

線グラフとアニメーション

// 線グラフの作成とアニメーション設定
class AnimatedLineChart {
    constructor(canvasId) {
        this.ctx = document.getElementById(canvasId).getContext('2d');
        this.chart = null;
        this.createChart();
    }
    
    createChart() {
        const data = {
            labels: ['1月', '2月', '3月', '4月', '5月', '6月'],
            datasets: [{
                label: '売上 (万円)',
                data: [65, 59, 80, 81, 56, 55],
                borderColor: 'rgb(75, 192, 192)',
                backgroundColor: 'rgba(75, 192, 192, 0.1)',
                tension: 0.4,
                fill: true,
                pointRadius: 6,
                pointHoverRadius: 8,
                pointBorderWidth: 2,
                pointBorderColor: '#fff'
            }, {
                label: '利益 (万円)',
                data: [28, 48, 40, 19, 86, 27],
                borderColor: 'rgb(255, 99, 132)',
                backgroundColor: 'rgba(255, 99, 132, 0.1)',
                tension: 0.4,
                fill: true,
                pointRadius: 6,
                pointHoverRadius: 8,
                pointBorderWidth: 2,
                pointBorderColor: '#fff'
            }]
        };
        
        const config = {
            type: 'line',
            data: data,
            options: {
                responsive: true,
                maintainAspectRatio: false,
                interaction: {
                    intersect: false,
                    mode: 'index'
                },
                plugins: {
                    title: {
                        display: true,
                        text: '月別売上・利益推移',
                        font: {
                            size: 16,
                            weight: 'bold'
                        },
                        padding: 20
                    },
                    legend: {
                        display: true,
                        position: 'top',
                        labels: {
                            usePointStyle: true,
                            padding: 15
                        }
                    },
                    tooltip: {
                        backgroundColor: 'rgba(0, 0, 0, 0.8)',
                        titleColor: '#fff',
                        bodyColor: '#fff',
                        borderColor: 'rgba(255, 255, 255, 0.1)',
                        borderWidth: 1,
                        cornerRadius: 5,
                        displayColors: true,
                        callbacks: {
                            label: function(context) {
                                return `${context.dataset.label}: ${context.parsed.y}万円`;
                            }
                        }
                    }
                },
                scales: {
                    x: {
                        display: true,
                        title: {
                            display: true,
                            text: '月'
                        },
                        grid: {
                            color: 'rgba(0, 0, 0, 0.1)'
                        }
                    },
                    y: {
                        display: true,
                        title: {
                            display: true,
                            text: '金額 (万円)'
                        },
                        grid: {
                            color: 'rgba(0, 0, 0, 0.1)'
                        },
                        beginAtZero: true
                    }
                },
                animation: {
                    duration: 2000,
                    easing: 'easeInOutQuart',
                    onComplete: function() {
                        console.log('アニメーション完了');
                    }
                },
                hover: {
                    animationDuration: 300
                }
            }
        };
        
        this.chart = new Chart(this.ctx, config);
    }
    
    updateData(newData) {
        this.chart.data.datasets[0].data = newData.sales;
        this.chart.data.datasets[1].data = newData.profit;
        this.chart.update('active');
    }
    
    addDataPoint(label, salesValue, profitValue) {
        this.chart.data.labels.push(label);
        this.chart.data.datasets[0].data.push(salesValue);
        this.chart.data.datasets[1].data.push(profitValue);
        this.chart.update();
    }
    
    removeFirstDataPoint() {
        this.chart.data.labels.shift();
        this.chart.data.datasets[0].data.shift();
        this.chart.data.datasets[1].data.shift();
        this.chart.update();
    }
    
    destroy() {
        if (this.chart) {
            this.chart.destroy();
        }
    }
}

// 使用例
const lineChart = new AnimatedLineChart('lineChart');

// データ更新の例
setTimeout(() => {
    lineChart.updateData({
        sales: [70, 65, 85, 75, 60, 65],
        profit: [35, 45, 42, 25, 80, 30]
    });
}, 3000);

ドーナツチャートとインタラクション

class InteractiveDonutChart {
    constructor(canvasId, data) {
        this.ctx = document.getElementById(canvasId).getContext('2d');
        this.data = data;
        this.chart = null;
        this.createChart();
    }
    
    createChart() {
        const config = {
            type: 'doughnut',
            data: {
                labels: this.data.labels,
                datasets: [{
                    data: this.data.values,
                    backgroundColor: [
                        '#FF6384',
                        '#36A2EB',
                        '#FFCE56',
                        '#4BC0C0',
                        '#9966FF',
                        '#FF9F40',
                        '#FF6384',
                        '#C9CBCF'
                    ],
                    borderColor: '#fff',
                    borderWidth: 3,
                    hoverOffset: 15,
                    hoverBorderWidth: 4
                }]
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                cutout: '60%',
                plugins: {
                    title: {
                        display: true,
                        text: '製品別売上構成比',
                        font: {
                            size: 18,
                            weight: 'bold'
                        },
                        padding: {
                            top: 10,
                            bottom: 30
                        }
                    },
                    legend: {
                        display: true,
                        position: 'right',
                        labels: {
                            usePointStyle: true,
                            padding: 15,
                            font: {
                                size: 12
                            },
                            generateLabels: function(chart) {
                                const data = chart.data;
                                if (data.labels.length && data.datasets.length) {
                                    return data.labels.map((label, i) => {
                                        const value = data.datasets[0].data[i];
                                        const total = data.datasets[0].data.reduce((a, b) => a + b, 0);
                                        const percentage = ((value / total) * 100).toFixed(1);
                                        
                                        return {
                                            text: `${label}: ${percentage}%`,
                                            fillStyle: data.datasets[0].backgroundColor[i],
                                            strokeStyle: data.datasets[0].borderColor,
                                            lineWidth: data.datasets[0].borderWidth,
                                            index: i
                                        };
                                    });
                                }
                                return [];
                            }
                        },
                        onClick: this.legendClickHandler.bind(this)
                    },
                    tooltip: {
                        backgroundColor: 'rgba(0, 0, 0, 0.8)',
                        titleColor: '#fff',
                        bodyColor: '#fff',
                        borderColor: 'rgba(255, 255, 255, 0.1)',
                        borderWidth: 1,
                        cornerRadius: 8,
                        displayColors: true,
                        callbacks: {
                            label: function(context) {
                                const label = context.label || '';
                                const value = context.parsed;
                                const total = context.dataset.data.reduce((a, b) => a + b, 0);
                                const percentage = ((value / total) * 100).toFixed(1);
                                return `${label}: ${value}個 (${percentage}%)`;
                            },
                            afterLabel: function(context) {
                                const total = context.dataset.data.reduce((a, b) => a + b, 0);
                                return `全体: ${total}個`;
                            }
                        }
                    }
                },
                animation: {
                    animateRotate: true,
                    animateScale: true,
                    duration: 1500,
                    easing: 'easeOutQuart'
                },
                hover: {
                    animationDuration: 300
                },
                onHover: this.hoverHandler.bind(this),
                onClick: this.clickHandler.bind(this)
            },
            plugins: [{
                id: 'centerText',
                beforeDraw: this.drawCenterText.bind(this)
            }]
        };
        
        this.chart = new Chart(this.ctx, config);
    }
    
    drawCenterText(chart) {
        const { ctx, chartArea: { left, top, width, height } } = chart;
        
        ctx.save();
        
        // 中央のテキスト描画
        const total = chart.data.datasets[0].data.reduce((a, b) => a + b, 0);
        
        ctx.font = 'bold 24px Arial';
        ctx.fillStyle = '#333';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        
        const centerX = left + width / 2;
        const centerY = top + height / 2;
        
        ctx.fillText(total.toString(), centerX, centerY - 10);
        
        ctx.font = '14px Arial';
        ctx.fillStyle = '#666';
        ctx.fillText('総売上個数', centerX, centerY + 15);
        
        ctx.restore();
    }
    
    hoverHandler(event, activeElements) {
        if (activeElements.length > 0) {
            const element = activeElements[0];
            const dataIndex = element.index;
            const value = this.chart.data.datasets[0].data[dataIndex];
            const label = this.chart.data.labels[dataIndex];
            
            // カスタムホバー処理
            console.log(`ホバー: ${label} - ${value}個`);
        }
    }
    
    clickHandler(event, activeElements) {
        if (activeElements.length > 0) {
            const element = activeElements[0];
            const dataIndex = element.index;
            const value = this.chart.data.datasets[0].data[dataIndex];
            const label = this.chart.data.labels[dataIndex];
            
            // クリック時の詳細表示
            this.showDetailModal(label, value);
        }
    }
    
    legendClickHandler(event, legendItem, legend) {
        const index = legendItem.index;
        const chart = legend.chart;
        
        // セグメントの表示/非表示切り替え
        const meta = chart.getDatasetMeta(0);
        meta.data[index].hidden = !meta.data[index].hidden;
        chart.update();
    }
    
    showDetailModal(label, value) {
        // モーダル表示(実装例)
        const modal = document.createElement('div');
        modal.style.cssText = `
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.3);
            z-index: 1000;
        `;
        
        modal.innerHTML = `
            <h3>${label} の詳細情報</h3>
            <p>売上個数: ${value}個</p>
            <p>シェア: ${((value / this.chart.data.datasets[0].data.reduce((a, b) => a + b, 0)) * 100).toFixed(1)}%</p>
            <button onclick="this.parentElement.remove()">閉じる</button>
        `;
        
        document.body.appendChild(modal);
        
        // 背景オーバーレイ
        const overlay = document.createElement('div');
        overlay.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.5);
            z-index: 999;
        `;
        overlay.onclick = () => {
            modal.remove();
            overlay.remove();
        };
        
        document.body.appendChild(overlay);
    }
    
    updateData(newLabels, newValues) {
        this.chart.data.labels = newLabels;
        this.chart.data.datasets[0].data = newValues;
        this.chart.update();
    }
    
    exportAsImage() {
        const url = this.chart.toBase64Image();
        const link = document.createElement('a');
        link.download = 'donut-chart.png';
        link.href = url;
        link.click();
    }
    
    destroy() {
        if (this.chart) {
            this.chart.destroy();
        }
    }
}

// 使用例
const donutData = {
    labels: ['Product A', 'Product B', 'Product C', 'Product D', 'Product E'],
    values: [30, 25, 20, 15, 10]
};

const donutChart = new InteractiveDonutChart('donutChart', donutData);

// データ更新の例
document.getElementById('updateButton').addEventListener('click', () => {
    donutChart.updateData(
        ['新商品A', '新商品B', '新商品C'],
        [40, 35, 25]
    );
});

// 画像エクスポートの例
document.getElementById('exportButton').addEventListener('click', () => {
    donutChart.exportAsImage();
});

混合チャート(棒グラフ + 線グラフ)

class MixedChart {
    constructor(canvasId) {
        this.ctx = document.getElementById(canvasId).getContext('2d');
        this.chart = null;
        this.createChart();
    }
    
    createChart() {
        const data = {
            labels: ['Q1', 'Q2', 'Q3', 'Q4'],
            datasets: [{
                type: 'bar',
                label: '売上 (百万円)',
                data: [120, 150, 180, 200],
                backgroundColor: 'rgba(54, 162, 235, 0.6)',
                borderColor: 'rgba(54, 162, 235, 1)',
                borderWidth: 2,
                yAxisID: 'y'
            }, {
                type: 'bar',
                label: 'コスト (百万円)',
                data: [80, 90, 110, 120],
                backgroundColor: 'rgba(255, 99, 132, 0.6)',
                borderColor: 'rgba(255, 99, 132, 1)',
                borderWidth: 2,
                yAxisID: 'y'
            }, {
                type: 'line',
                label: '利益率 (%)',
                data: [33.3, 40.0, 38.9, 40.0],
                borderColor: 'rgba(75, 192, 192, 1)',
                backgroundColor: 'rgba(75, 192, 192, 0.1)',
                borderWidth: 3,
                fill: false,
                tension: 0.3,
                pointRadius: 6,
                pointHoverRadius: 8,
                pointBorderWidth: 2,
                pointBorderColor: '#fff',
                yAxisID: 'y1'
            }]
        };
        
        const config = {
            type: 'bar',
            data: data,
            options: {
                responsive: true,
                maintainAspectRatio: false,
                interaction: {
                    intersect: false,
                    mode: 'index'
                },
                plugins: {
                    title: {
                        display: true,
                        text: '四半期業績推移(売上・コスト・利益率)',
                        font: {
                            size: 16,
                            weight: 'bold'
                        }
                    },
                    legend: {
                        display: true,
                        position: 'top',
                        labels: {
                            usePointStyle: true,
                            filter: function(item, chart) {
                                return true;
                            }
                        }
                    },
                    tooltip: {
                        backgroundColor: 'rgba(0, 0, 0, 0.8)',
                        titleColor: '#fff',
                        bodyColor: '#fff',
                        multiKeyBackground: 'rgba(255, 255, 255, 0.1)',
                        callbacks: {
                            label: function(context) {
                                if (context.datasetIndex === 2) {
                                    return `${context.dataset.label}: ${context.parsed.y}%`;
                                } else {
                                    return `${context.dataset.label}: ${context.parsed.y}百万円`;
                                }
                            },
                            afterBody: function(tooltipItems) {
                                const dataIndex = tooltipItems[0].dataIndex;
                                const sales = tooltipItems.find(item => item.datasetIndex === 0)?.parsed.y || 0;
                                const cost = tooltipItems.find(item => item.datasetIndex === 1)?.parsed.y || 0;
                                const profit = sales - cost;
                                return [`純利益: ${profit}百万円`];
                            }
                        }
                    }
                },
                scales: {
                    x: {
                        display: true,
                        title: {
                            display: true,
                            text: '四半期'
                        }
                    },
                    y: {
                        type: 'linear',
                        display: true,
                        position: 'left',
                        title: {
                            display: true,
                            text: '金額 (百万円)'
                        },
                        grid: {
                            color: 'rgba(0, 0, 0, 0.1)'
                        },
                        beginAtZero: true
                    },
                    y1: {
                        type: 'linear',
                        display: true,
                        position: 'right',
                        title: {
                            display: true,
                            text: '利益率 (%)'
                        },
                        grid: {
                            drawOnChartArea: false
                        },
                        min: 0,
                        max: 50
                    }
                },
                animation: {
                    duration: 2000,
                    delay: (context) => {
                        let delay = 0;
                        if (context.type === 'data' && context.mode === 'default') {
                            delay = context.dataIndex * 200 + context.datasetIndex * 100;
                        }
                        return delay;
                    }
                }
            }
        };
        
        this.chart = new Chart(this.ctx, config);
    }
    
    addQuarter(quarter, sales, cost, profitRate) {
        this.chart.data.labels.push(quarter);
        this.chart.data.datasets[0].data.push(sales);
        this.chart.data.datasets[1].data.push(cost);
        this.chart.data.datasets[2].data.push(profitRate);
        this.chart.update();
    }
    
    updateQuarter(index, sales, cost, profitRate) {
        if (index < this.chart.data.labels.length) {
            this.chart.data.datasets[0].data[index] = sales;
            this.chart.data.datasets[1].data[index] = cost;
            this.chart.data.datasets[2].data[index] = profitRate;
            this.chart.update();
        }
    }
    
    generateRandomData() {
        const quarters = this.chart.data.labels;
        quarters.forEach((quarter, index) => {
            const sales = Math.floor(Math.random() * 100) + 100;
            const cost = Math.floor(sales * (0.6 + Math.random() * 0.2));
            const profitRate = ((sales - cost) / sales * 100);
            
            this.updateQuarter(index, sales, cost, profitRate);
        });
    }
    
    destroy() {
        if (this.chart) {
            this.chart.destroy();
        }
    }
}

// 使用例
const mixedChart = new MixedChart('mixedChart');

// ランダムデータ生成ボタン
document.getElementById('randomizeButton').addEventListener('click', () => {
    mixedChart.generateRandomData();
});

// 新しい四半期データ追加
document.getElementById('addQuarterButton').addEventListener('click', () => {
    const quarterCount = mixedChart.chart.data.labels.length;
    const sales = Math.floor(Math.random() * 100) + 150;
    const cost = Math.floor(sales * 0.7);
    const profitRate = ((sales - cost) / sales * 100);
    
    mixedChart.addQuarter(`Q${quarterCount + 1}`, sales, cost, profitRate);
});

リアルタイムデータ更新

class RealTimeChart {
    constructor(canvasId) {
        this.ctx = document.getElementById(canvasId).getContext('2d');
        this.chart = null;
        this.isRunning = false;
        this.maxDataPoints = 20;
        this.updateInterval = null;
        this.createChart();
    }
    
    createChart() {
        const initialData = Array.from({length: this.maxDataPoints}, (_, i) => ({
            x: new Date(Date.now() - (this.maxDataPoints - 1 - i) * 1000),
            y: Math.random() * 100
        }));
        
        const config = {
            type: 'line',
            data: {
                datasets: [{
                    label: 'CPU使用率 (%)',
                    data: initialData,
                    borderColor: 'rgb(255, 99, 132)',
                    backgroundColor: 'rgba(255, 99, 132, 0.1)',
                    tension: 0.4,
                    fill: true,
                    pointRadius: 0,
                    borderWidth: 2
                }, {
                    label: 'メモリ使用率 (%)',
                    data: initialData.map(point => ({
                        x: point.x,
                        y: Math.random() * 80 + 10
                    })),
                    borderColor: 'rgb(54, 162, 235)',
                    backgroundColor: 'rgba(54, 162, 235, 0.1)',
                    tension: 0.4,
                    fill: true,
                    pointRadius: 0,
                    borderWidth: 2
                }]
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                plugins: {
                    title: {
                        display: true,
                        text: 'システムモニタリング(リアルタイム)'
                    },
                    legend: {
                        display: true
                    }
                },
                scales: {
                    x: {
                        type: 'time',
                        time: {
                            displayFormats: {
                                second: 'HH:mm:ss'
                            },
                            tooltipFormat: 'HH:mm:ss'
                        },
                        title: {
                            display: true,
                            text: '時刻'
                        }
                    },
                    y: {
                        beginAtZero: true,
                        max: 100,
                        title: {
                            display: true,
                            text: '使用率 (%)'
                        }
                    }
                },
                animation: {
                    duration: 0
                },
                elements: {
                    point: {
                        radius: 0
                    }
                }
            }
        };
        
        this.chart = new Chart(this.ctx, config);
    }
    
    addDataPoint() {
        const now = new Date();
        const cpuUsage = Math.random() * 100;
        const memoryUsage = Math.random() * 80 + 10;
        
        // 新しいデータポイントを追加
        this.chart.data.datasets[0].data.push({
            x: now,
            y: cpuUsage
        });
        
        this.chart.data.datasets[1].data.push({
            x: now,
            y: memoryUsage
        });
        
        // 古いデータポイントを削除
        if (this.chart.data.datasets[0].data.length > this.maxDataPoints) {
            this.chart.data.datasets[0].data.shift();
            this.chart.data.datasets[1].data.shift();
        }
        
        // チャートを更新
        this.chart.update('none');
        
        // 現在値を表示
        this.displayCurrentValues(cpuUsage, memoryUsage);
    }
    
    displayCurrentValues(cpu, memory) {
        const statusDiv = document.getElementById('status');
        if (statusDiv) {
            statusDiv.innerHTML = `
                <div>CPU: ${cpu.toFixed(1)}%</div>
                <div>メモリ: ${memory.toFixed(1)}%</div>
                <div>更新時刻: ${new Date().toLocaleTimeString()}</div>
            `;
        }
    }
    
    start() {
        if (!this.isRunning) {
            this.isRunning = true;
            this.updateInterval = setInterval(() => {
                this.addDataPoint();
            }, 1000);
        }
    }
    
    stop() {
        if (this.isRunning) {
            this.isRunning = false;
            if (this.updateInterval) {
                clearInterval(this.updateInterval);
                this.updateInterval = null;
            }
        }
    }
    
    clear() {
        this.chart.data.datasets[0].data = [];
        this.chart.data.datasets[1].data = [];
        this.chart.update();
    }
    
    setUpdateInterval(milliseconds) {
        if (this.isRunning) {
            this.stop();
            setTimeout(() => {
                this.start();
            }, 100);
        }
        
        // 次回開始時のために保存
        this.updateIntervalMs = milliseconds;
    }
    
    exportData() {
        const data = this.chart.data.datasets.map((dataset, index) => ({
            label: dataset.label,
            data: dataset.data.map(point => ({
                time: point.x.toISOString(),
                value: point.y.toFixed(2)
            }))
        }));
        
        const blob = new Blob([JSON.stringify(data, null, 2)], {
            type: 'application/json'
        });
        
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'realtime_data.json';
        a.click();
        URL.revokeObjectURL(url);
    }
    
    destroy() {
        this.stop();
        if (this.chart) {
            this.chart.destroy();
        }
    }
}

// 使用例
const realtimeChart = new RealTimeChart('realtimeChart');

// 制御ボタン
document.getElementById('startButton').addEventListener('click', () => {
    realtimeChart.start();
});

document.getElementById('stopButton').addEventListener('click', () => {
    realtimeChart.stop();
});

document.getElementById('clearButton').addEventListener('click', () => {
    realtimeChart.clear();
});

document.getElementById('exportButton').addEventListener('click', () => {
    realtimeChart.exportData();
});

// 更新間隔の設定
document.getElementById('intervalSelect').addEventListener('change', (e) => {
    const interval = parseInt(e.target.value);
    realtimeChart.setUpdateInterval(interval);
});

レスポンシブ設計とプラグイン

class ResponsiveChartManager {
    constructor() {
        this.charts = new Map();
        this.setupResizeHandler();
    }
    
    createResponsiveChart(canvasId, config) {
        const ctx = document.getElementById(canvasId).getContext('2d');
        
        // レスポンシブ設定を強化
        const responsiveConfig = {
            ...config,
            options: {
                ...config.options,
                responsive: true,
                maintainAspectRatio: false,
                plugins: {
                    ...config.options?.plugins,
                    // カスタムプラグインを追加
                    customResponsive: {
                        onResize: this.handleChartResize.bind(this)
                    }
                }
            }
        };
        
        const chart = new Chart(ctx, responsiveConfig);
        this.charts.set(canvasId, chart);
        
        return chart;
    }
    
    handleChartResize(chart, size) {
        console.log(`Chart ${chart.canvas.id} resized to ${size.width}x${size.height}`);
        
        // 画面サイズに応じて設定を調整
        if (size.width < 600) {
            // モバイル向け設定
            this.applyMobileSettings(chart);
        } else if (size.width < 1024) {
            // タブレット向け設定
            this.applyTabletSettings(chart);
        } else {
            // デスクトップ向け設定
            this.applyDesktopSettings(chart);
        }
        
        chart.update('none');
    }
    
    applyMobileSettings(chart) {
        const options = chart.options;
        
        // フォントサイズを縮小
        if (options.plugins?.title) {
            options.plugins.title.font = { size: 14 };
        }
        
        if (options.plugins?.legend) {
            options.plugins.legend.position = 'bottom';
            options.plugins.legend.labels.font = { size: 10 };
        }
        
        // 軸のラベルを簡略化
        if (options.scales?.x?.title) {
            options.scales.x.title.display = false;
        }
        if (options.scales?.y?.title) {
            options.scales.y.title.display = false;
        }
    }
    
    applyTabletSettings(chart) {
        const options = chart.options;
        
        if (options.plugins?.title) {
            options.plugins.title.font = { size: 16 };
        }
        
        if (options.plugins?.legend) {
            options.plugins.legend.position = 'top';
            options.plugins.legend.labels.font = { size: 12 };
        }
        
        if (options.scales?.x?.title) {
            options.scales.x.title.display = true;
        }
        if (options.scales?.y?.title) {
            options.scales.y.title.display = true;
        }
    }
    
    applyDesktopSettings(chart) {
        const options = chart.options;
        
        if (options.plugins?.title) {
            options.plugins.title.font = { size: 18 };
        }
        
        if (options.plugins?.legend) {
            options.plugins.legend.position = 'top';
            options.plugins.legend.labels.font = { size: 14 };
        }
    }
    
    setupResizeHandler() {
        let resizeTimeout;
        window.addEventListener('resize', () => {
            clearTimeout(resizeTimeout);
            resizeTimeout = setTimeout(() => {
                this.charts.forEach((chart, canvasId) => {
                    const canvas = document.getElementById(canvasId);
                    if (canvas) {
                        const rect = canvas.getBoundingClientRect();
                        this.handleChartResize(chart, {
                            width: rect.width,
                            height: rect.height
                        });
                    }
                });
            }, 250);
        });
    }
    
    // カスタムプラグイン: グラデーション背景
    createGradientPlugin() {
        return {
            id: 'gradientBackground',
            beforeDraw: (chart) => {
                const { ctx, chartArea } = chart;
                if (!chartArea) return;
                
                ctx.save();
                const gradient = ctx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);
                gradient.addColorStop(0, 'rgba(54, 162, 235, 0.1)');
                gradient.addColorStop(1, 'rgba(54, 162, 235, 0.05)');
                
                ctx.fillStyle = gradient;
                ctx.fillRect(chartArea.left, chartArea.top, chartArea.width, chartArea.height);
                ctx.restore();
            }
        };
    }
    
    // カスタムプラグイン: ウォーターマーク
    createWatermarkPlugin(text = 'Chart.js Demo') {
        return {
            id: 'watermark',
            afterDraw: (chart) => {
                const { ctx, chartArea } = chart;
                if (!chartArea) return;
                
                ctx.save();
                ctx.globalAlpha = 0.1;
                ctx.font = 'bold 20px Arial';
                ctx.fillStyle = '#999';
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                
                const centerX = chartArea.left + chartArea.width / 2;
                const centerY = chartArea.top + chartArea.height / 2;
                
                ctx.fillText(text, centerX, centerY);
                ctx.restore();
            }
        };
    }
    
    // すべてのチャートを破棄
    destroyAll() {
        this.charts.forEach(chart => chart.destroy());
        this.charts.clear();
    }
    
    // 特定のチャートを取得
    getChart(canvasId) {
        return this.charts.get(canvasId);
    }
    
    // チャートの一覧を取得
    getAllCharts() {
        return Array.from(this.charts.values());
    }
}

// プラグインを登録
Chart.register(
    // カスタムプラグインをグローバルに登録
    {
        id: 'customResponsive',
        resize: function(chart, size, options) {
            if (options.onResize) {
                options.onResize(chart, size);
            }
        }
    }
);

// 使用例
const chartManager = new ResponsiveChartManager();

// グラデーション背景プラグインを登録
Chart.register(chartManager.createGradientPlugin());

// レスポンシブチャートを作成
const responsiveChart = chartManager.createResponsiveChart('responsiveChart', {
    type: 'line',
    data: {
        labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
        datasets: [{
            label: 'データセット1',
            data: [65, 59, 80, 81, 56, 55],
            borderColor: 'rgb(75, 192, 192)',
            tension: 0.1
        }]
    },
    options: {
        plugins: {
            title: {
                display: true,
                text: 'レスポンシブチャート'
            }
        }
    }
});

// ウォーターマーク付きチャート
const watermarkChart = chartManager.createResponsiveChart('watermarkChart', {
    type: 'bar',
    data: {
        labels: ['A', 'B', 'C', 'D'],
        datasets: [{
            label: 'サンプルデータ',
            data: [12, 19, 3, 5],
            backgroundColor: 'rgba(255, 99, 132, 0.8)'
        }]
    },
    plugins: [chartManager.createWatermarkPlugin('My Company')]
});