Chart.js
Canvas要素を使用したレスポンシブなチャートライブラリ。8種類の基本チャート、アニメーション、インタラクション機能を提供。シンプルなAPI設計により、迅速なデータ可視化が可能。
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
スター履歴
データ取得日時: 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非対応: ベクター形式での出力が困難
主要リンク
- Chart.js公式サイト
- Chart.js GitHub リポジトリ
- Chart.js ドキュメント
- Chart.js サンプル集
- Chart.js コミュニティプラグイン
- Chart.js Slack コミュニティ
書き方の例
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')]
});