vue-chartjs（chart.js）で縦軸のタイトルを縦書きにする

## やりたいこと

![スクリーンショット 2023-11-22 11.19.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/368222/f975fea1-f2e4-564c-5be7-f9a8a531e2e5.png)

## 動作環境

```json:package.json
    "vue": "^3.3.8",
    "vue-chartjs": "^4.0.4",
    "chart.js": "^3.7.1",
```

## ソースコード

正規分布図を描画するコンポーネント。

```vue:NormalDistributionChart.vue
<template>
  <Line
    :chart-data="chartData"
    :chart-options="chartOptions"
    :plugins="plugins"
  />
</template>

<script setup lang="ts">
import { defineProps, withDefaults, computed } from "vue";
import { Line } from "vue-chartjs";
import {
  Chart,
  ChartData,
  ChartOptions,
  Plugin,
  registerables,
} from "chart.js";

Chart.register(...registerables);

export interface LineData {
  fx: number[];
  label: string;
  color: string;
}

export interface Props {
  x: number[];
  lines: LineData[];
}

const props = withDefaults(defineProps<Props>(), {
  x: () => [],
  lines: () => [],
});

const chartData = computed<ChartData<"line">>(() => ({
  labels: props.x,
  datasets: props.lines.map((line) => {
    return {
      label: line.label,
      data: line.fx,
      backgroundColor: line.color,
      borderColor: line.color,
      borderWidth: 3,
    };
  }),
}));

const chartOptions = computed<ChartOptions<"line">>(() => ({
  responsive: true,
  maintainAspectRatio: false,
  scales: {
    x: {
      title: {
        display: true,
        text: "横軸ラベル",
        font: {
          size: 14,
          weight: "bold",
        },
      },
    },
    y: {
      ticks: {
        callback: (value) => `${Math.floor(Number(value) * 100)}%`,
      },
    },
  },
  plugins: {
    legend: {
      align: "end",
      labels: {
        usePointStyle: true,
        font: {
          size: 12,
        },
      },
    },
    verticalTitle: {
      display: true,
      text: "縦軸ラベル",
      font: {
        size: 14,
        weight: "bold",
      },
    },
  },
}));

const plugins: Plugin<"line">[] = [
  {
    id: "verticalTitle",
    beforeLayout: (chart: any, args: any, opts: any) => {
      const { display, font } = opts;
      if (!display) return;

      const { ctx } = chart;
      ctx.font = `${font?.weight || "normal"} ${font?.size || 12}px ${
        font?.family || "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"
      }`;
      const { width } = ctx.measureText(opts.text[0]);
      chart.options.layout.padding.left = width * 1.5;
    },
    afterDraw: (chart: any, args: any, opts: any) => {
      const { display, font, text, color } = opts;
      const {
        ctx,
        chartArea: { top, bottom, left, right },
      } = chart;

      if (!display) return;

      ctx.fillStyle = color || Chart.defaults.color;
      ctx.font = `${font?.weight || "normal"} ${font?.size || 12}px ${
        font?.family || "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"
      }`;

      const fontSize = font?.size || 12;
      const y = (top + bottom) / 2 - (text.length / 2) * fontSize;
      const lineHeight = 1.1618; // 行の高さ (フォントサイズに対する倍率)
      for (var lines = text.split(""), i = 0, l = lines.length; l > i; i++) {
        var line = lines[i];
        var addY = fontSize;

        // 2行目以降の水平位置は行数とlineHeightを考慮する
        if (i) addY += fontSize * lineHeight * i;

        ctx.fillText(line, 0, y + addY);
      }
    },
  },
];
</script>
```

## ポイント

通常機能として提供されていないので plugins で縦書きの縦軸ラベル機能を追加する。

"verticalTitle"という名前で追加するコード。

```vue:NormalDistributionChart.vue
<script setup lang="ts">
const plugins: Plugin<"line">[] = [
  {
    id: "verticalTitle",
    beforeLayout: (chart: any, args: any, opts: any) => {
      const { display, font } = opts;
      if (!display) return;

      const { ctx } = chart;
      ctx.font = `${font?.weight || "normal"} ${font?.size || 12}px ${
        font?.family || "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"
      }`;
      const { width } = ctx.measureText(opts.text[0]);
      chart.options.layout.padding.left = width * 1.5;
    },
    afterDraw: (chart: any, args: any, opts: any) => {
      const { display, font, text, color } = opts;
      const {
        ctx,
        chartArea: { top, bottom, left, right },
      } = chart;

      if (!display) return;

      ctx.fillStyle = color || Chart.defaults.color;
      ctx.font = `${font?.weight || "normal"} ${font?.size || 12}px ${
        font?.family || "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"
      }`;

      const fontSize = font?.size || 12;
      const y = (top + bottom) / 2 - (text.length / 2) * fontSize;
      const lineHeight = 1.1618; // 行の高さ (フォントサイズに対する倍率)
      for (var lines = text.split(""), i = 0, l = lines.length; l > i; i++) {
        var line = lines[i];
        var addY = fontSize;

        // 2行目以降の水平位置は行数とlineHeightを考慮する
        if (i) addY += fontSize * lineHeight * i;

        ctx.fillText(line, 0, y + addY);
      }
    },
  },
];
</script>
```

"verticalTitle"を使うコード。

```vue:NormalDistributionChart.vue
<script setup lang="ts">
const chartOptions = computed<ChartOptions<"line">>(() => ({
  plugins: {
    verticalTitle: {
      display: true,
      text: "縦軸ラベル",
      font: {
        size: 14,
        weight: "bold",
      },
    },
  },
}));
</script>
```