はじめに
棒グラフ用に作っていたVueコンポーネントをベースに、ドーナツグラフ(円グラフ) への変更を行いました。Chart.js + vue-chartjsの組み合わせで実装しています。
ドーナツグラフ化の手順まとめ
変更点一覧
項目 | 修正内容 |
---|---|
インポート |
Bar → Doughnut
|
ChartJS登録 |
BarElement → ArcElement
|
コンポーネント |
<Bar /> → <Doughnut />
|
オプション |
scales 削除、tooltip/datalabels |
Chart.jsオプション設定(ドーナツ対応)
function createChartOptions() {
return {
responsive: true,
maintainAspectRatio: false,
cutout: '60%', // 中央の穴のサイズ
plugins: {
datalabels: {
color: '#333',
font: { weight: 'bold', size: 12 },
formatter: (value, context) => {
const data = context.chart.data.datasets[0].data;
const total = data.reduce((a, b) => a + b, 0);
const percentage = (value / total * 100).toFixed(1);
return `${percentage}%`;
}
},
legend: {
display: true,
position: 'right'
},
tooltip: {
callbacks: {
label: (context) => {
const label = context.label || '';
const value = context.parsed;
return `${label}: ¥${value.toLocaleString()}`;
}
}
}
},
animation: {
animateRotate: true,
animateScale: true,
duration: 1500
}
}
}
色設定(カラーパレット)
colors: () => [
'#FF6384', '#36A2EB', '#FFCE56',
'#4BC0C0', '#9966FF', '#FF9F40',
'#66FF66', '#FF66B2', '#C9CBCF',
'#FF6666'
]
props.colors
のdefault
で使います
よくあるエラーと対策
問題:「%」がすべて「0.0%」になる
原因:
APIレスポンスでtotals
が文字列(例:"5000"
)として帰ってきているため、合計や割合の計算が崩れる。
解決策:
数値に変換してから使う。
const totals = json.totals.map(t => Number(t));
実装コード例(修正版)
DoughnutChart.vue
<script setup>
import { ref, onMounted } from 'vue'
import { Doughnut } from 'vue-chartjs'
import {
Chart as ChartJS,
Title, Tooltip, Legend, ArcElement
} from 'chart.js'
import ChartDataLabels from 'chartjs-plugin-datalabels'
ChartJS.register(Title, Tooltip, Legend, ArcElement, ChartDataLabels)
const props = defineProps({
label: { type: String, default: 'カテゴリー別支出合計' },
apiUrl: { type: String, default: 'api/category-chart-data' },
colors: {
type: Array,
default: () => [
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF',
'#FF9F40', '#66FF66', '#FF66B2', '#C9CBCF', '#FF6666'
]
}
})
const chartData = ref({ labels: [], datasets: [] })
const chartOptions = ref(createChartOptions())
function createChartOptions() {
return {
responsive: true,
maintainAspectRatio: false,
cutout: '60%',
plugins: {
datalabels: {
color: '#333',
font: { weight: 'bold', size: 12 },
formatter: (value, context) => {
const data = context.chart.data.datasets[0].data;
const total = data.reduce((a, b) => a + b, 0);
const percentage = (value / total * 100).toFixed(1);
return `${percentage}%`;
}
},
legend: {
display: true,
position: 'right'
},
tooltip: {
callbacks: {
label: (context) => {
const label = context.label || '';
const value = context.parsed;
return `${label}: ¥${value.toLocaleString()}`;
}
}
}
},
animation: {
animateRotate: true,
animateScale: true,
duration: 1500
}
}
}
onMounted(async () => {
try {
const res = await fetch(props.apiUrl)
const json = await res.json()
chartData.value = {
labels: json.labels,
datasets: [
{
label: props.label,
data: json.totals.map(t => Number(t)),
backgroundColor: props.colors.slice(0, json.labels.length)
}
]
}
} catch (e) {
console.error('データ取得エラー:', e)
}
})
</script>
<template>
<div style="height: 400px;">
<Doughnut :data="chartData" :options="chartOptions" />
</div>
</template>
Laravel側:API実装例
ChartController.php
use Carbon\Carbon;
public function getCategoryChartData()
{
$now = Carbon::now();
$data = Expense::select('category_id', DB::raw('SUM(amount) as total'))
->whereYear('date', $now->year)
->whereMonth('date', $now->month)
->groupBy('category_id')
->with('category')
->get();
return response()->json([
'labels' => $data->pluck('category.name'),
'totals' => $data->pluck('total')
]);
}
今月の支出合計を表示する
Controller側(Laravel側)
ChartController.php
$totalExpense = Expense::whereYear('date', $now->year)
->whereMonth('date', $now->month)
->sum('amount');
return Inertia::render('Dashboard', [
// その他のデータ
'totalExpense' => $totalExpense
]);
Vue側
DoughnutChart.vue
<script setup>
import { computed } from 'vue'
const props = defineProps({
totalExpense: Number
})
const formattedTotal = computed(() => {
return Number(props.totalExpense).toLocaleString();
})
</script>
<template>
<p>今月の合計支出: {{ formattedTotal }}円</p>
</template>
Composableで共通化(useCurrency)
// composables/useCurrency.js
export function useCurrency() {
const formatCurrency = (value) => {
const num = Number(value);
if (isNaN(num)) return '0円';
return num.toLocaleString() + '円';
};
return { formatCurrency };
}
使用例
<script setup>
import { useCurrency } from '@/composables/useCurrency';
const { formatCurrency } = useCurrency();
</script>
<template>
<p>支出合計: {{ formatCurrency(25000) }}</p>
</template>
おわりに
- 棒グラフ→ドーナツグラフの変更は意外とシンプル
- Chart.jsは柔軟にカスタマイズできる
- Composableやpropsの使い方を工夫うすれば再利用性も高くなる
Vue + Chart.jsでグラフを使いたい方の参考になれば幸いです。