1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Nuxt Composition Api で Chart.js を使ってグラフを描画する。

Last updated at Posted at 2021-09-10

データ更新に伴ってチャートを更新するという実装に、盛大にハマったため備忘録として、試行錯誤の経緯とともに記載します。
親コンポーネントから値を渡して、子コンポーネントでその値を使ってチャートを描画する構成です。
結論だけ知りたい方は、こちら

まずは、リアクティブなデータを渡して描画してみる。

これがベースになります。<canvas>ctxChart オブジェクトの生成は mounted の中でやらないといけないことと、
canvasRefreturn しないといけないので注意。

index.vue
<template>
  <div>
    <TestComponent :data="data"/>
  </div>
</template>

<script lang="ts">
import {defineComponent, ref} from '@nuxtjs/composition-api';
import TestComponent from '~/components/TestComponent.vue';

export default defineComponent({
  components: {TestComponent},
  setup () {
    const data = ref([2, 3]);
    return {data};
  }
});
</script>
TestComponent.vue
<template>
  <div>
    <canvas ref="canvasRef" />
  </div>
</template>

<script lang="ts">
import Chart from 'chart.js';
import {defineComponent, onMounted, ref} from '@nuxtjs/composition-api';

export default defineComponent({
  name: 'TestComponent',
  props: {
    data: {
      type: Array,
      required: true
    }
  },
  setup (props) {
    const canvasRef = ref<HTMLCanvasElement | null>(null);

    onMounted(() => {
      // mounted の中じゃないと ctx が取れない
      const ctx = canvasRef.value?.getContext('2d');
      if (!ctx) return;

      // mounted の中じゃないと ctx が取れないので、Chart の生成も mounted の中じゃないといけないっぽい
      const chart = new Chart(ctx, {
        type: 'bar',
        data: {
          labels: ['東京', '大阪'],
          datasets: [{
            label: 'てすと',
            data: props.data
          }]
        }
      });
    });

    return {canvasRef}; // canvasRef をリターンしないと表示されない
  }
});
</script>

データを更新してみる。

ボタンとメソッドを追加して、data を更新しますが、これは Chart オブジェクトには反映されません...

index.vue
<template>
  <div>
    <button @click="updateData">グラフ更新</button>
    <TestComponent :data="data"/>
  </div>
</template>

<script lang="ts">
import {defineComponent, ref} from '@nuxtjs/composition-api';
import TestComponent from '~/components/TestComponent.vue';

export default defineComponent({
  components: {TestComponent},
  setup () {
    const data = ref([2, 3]);

    const updateData = () => {
      data.value = [4, 4];
    };

    return {data, updateData};
  }
});
</script>

Chart オブジェクトをリアクティブにしてみる。

あー、Chartオブジェクトをリアクティブにしないといけないのか。それでおしまい!って思ったけど、なぜかチャートは更新されません。

TestComponent.vue
<script lang="ts">
import Chart from 'chart.js';
import {defineComponent, onMounted, reactive, ref} from '@nuxtjs/composition-api';

... 中略 ...

    onMounted(() => {
      const ctx = canvasRef.value?.getContext('2d');
      if (!ctx) return;

      const chart = reactive<Chart>(new Chart(ctx, { // リアクティブにする
        type: 'bar',
        data: {
          labels: ['東京', '大阪'],
          datasets: [{
            label: 'てすと',
            data: props.data
          }]
        }
      }));
    });

    return {canvasRef};
  }
});
</script>

Date オブジェクトで試してみる

同じオブジェクトということで、一度 Dateオブジェクトで試してみましたが、親のtimeを更新しても、子のdateは更新されない。

index.vue
<template>
  <div>
    <button @click="changeTime">チェンジタイム</button>
    <TestComponent :time="time"/>
  </div>
</template>

<script lang="ts">
import {defineComponent, ref} from '@nuxtjs/composition-api';
import TestComponent from '~/components/TestComponent.vue';

export default defineComponent({
  components: {TestComponent},
  setup() {
    const time = ref<string>('2021-09-08 10:00');
    const changeTime = () => {
      time.value = '2021-09-08 11:00';
    };
    return {
      time,
      changeTime
    };
  },
});
</script>
TestComponent.vue
<template>
  <div>
    <p>{{ date }}</p>
  </div>
</template>

<script>
import {defineComponent, reactive} from '@nuxtjs/composition-api';

export default defineComponent({
  name: 'TestComponent',
  props: {
    time: {
      type: String,
      default: ''
    }
  },
  setup(props) {
    const date = reactive(new Date(props.time));
    return {date};
  }
});
</script>

値をcomputedで監視して、都度 Date オブジェクトを生成する

time が変更されるたびに、Date オブジェクトを生成するようにしたら変更が反映されました!なるほど!

TestComponent.vue
<script>
... 中略 ...
  setup(props) {
    const date = computed(() => new Date(props.time));
    return {date};
  }
... 中略 ...
</script>

上と同じことをしてみる。

上と同じようにして、これでやっと解決。と思いきやこれでもだめという....

index.vue
<template>
  <div>
    <button @click="updateData">グラフ更新</button>
    <TestComponent :data="data"/>
  </div>
</template>

<script lang="ts">
import {defineComponent, ref} from '@nuxtjs/composition-api';
import TestComponent from '~/components/TestComponent.vue';

export default defineComponent({
  components: {TestComponent},
  setup () {
    const data = ref([2, 3]);

    const updateData = () => {
      data.value = [4, 4];
    };

    return {data, updateData};
  }
});
</script>
TestComponent.vue
<template>
  <div>
    <canvas ref="canvasRef" />
  </div>
</template>

<script lang="ts">
import Chart from 'chart.js';
import {computed, defineComponent, onMounted, ref} from '@nuxtjs/composition-api';

export default defineComponent({
  name: 'TestComponent',
  props: {
    data: {
      type: Array,
      required: true
    }
  },
  setup (props) {
    const canvasRef = ref<HTMLCanvasElement | null>(null);

    onMounted(() => {
      const ctx = canvasRef.value?.getContext('2d');
      if (!ctx) return;

      const chart = computed(() => new Chart(ctx, {
        type: 'bar',
        data: {
          labels: ['東京', '大阪'],
          datasets: [{
            label: 'てすと',
            data: props.data
          }]
        }
      }));
    });

    return {canvasRef};
  }
});
</script>

困る

自分の知識、ググり力を駆使して、完全に手を尽くしたため、まじで困りました。

Chart を update する

これまで、Chart オブジェクトが、どうすればリアクティブになって、更新できるようになるかを考えていましたが、
最終的に、Chart オブジェクトに update() があることがわかり、
datawatch() で監視して、update() で更新するという実装に落ち着きました。
https://misc.0o0o.org/chartjs-doc-ja/developers/updates.html

TestComponent.vue
<template>
  <div>
    <canvas ref="canvasRef" />
  </div>
</template>

<script lang="ts">
import Chart from 'chart.js';
import {computed, defineComponent, onMounted, ref, toRefs, watch} from '@nuxtjs/composition-api';

export default defineComponent({
  name: 'TestComponent',
  props: {
    data: {
      type: Array,
      required: true
    }
  },
  setup (props) {
    const canvasRef = ref<HTMLCanvasElement | null>(null);

    onMounted(() => {
      const ctx = canvasRef.value?.getContext('2d');
      if (!ctx) return;

      const chart = new Chart(ctx, {
        type: 'bar',
        data: {
          labels: ['東京', '大阪'],
          datasets: [{
            label: 'てすと',
            data: props.data
          }]
        }
      });

      // props を直接監視できないので、toRefs で取り出す。
      const { data } = toRefs(props);
      watch(data, () => {
        chart.data.datasets[0].data = data.value;
        chart.update();
      });

      // ※ または、第一引数に props.data の getter 関数を渡す。
      // ※ https://v3.ja.vuejs.org/guide/reactivity-computed-watchers.html#watch
      watch(() => props.data, (newValue) => {
        chart.data.datasets[0].data = newValue;
        chart.update();
      });

      // data を computed で加工してから、利用する場合はこんな感じ。
      const computedData = computed(() => doSometing(props.data));
      watch(computedData, () => {
        chart.data.datasets[0].data = computedData.value; // computed の値は `.value` で取れる
        chart.update();
      });
    });

    return {canvasRef};
  }
});
</script>

まとめ

オブジェクトって、reactive()ってすれば、リアクティブになるんじゃないのかよーって感じです。
浅い知識で立ち向かってるので、なぜ狙い通りの挙動にならないのかわからないですし、
最終的にリアクティブにすることは諦めるということになったので、少々もやもやはあります。
もっとスマートな方法をご存知のかたいらっしゃったらコメントで教えていただけると幸いです。

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?