0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vue.js + Chart.js でドーナツグラフを作成する備忘録

Posted at

はじめに

棒グラフ用に作っていたVueコンポーネントをベースに、ドーナツグラフ(円グラフ) への変更を行いました。Chart.js + vue-chartjsの組み合わせで実装しています。

ドーナツグラフ化の手順まとめ

変更点一覧

項目 修正内容
インポート BarDoughnut
ChartJS登録 BarElementArcElement
コンポーネント <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.colorsdefaultで使います

よくあるエラーと対策

問題:「%」がすべて「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でグラフを使いたい方の参考になれば幸いです。
0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?