データ更新に伴ってチャートを更新するという実装に、盛大にハマったため備忘録として、試行錯誤の経緯とともに記載します。
親コンポーネントから値を渡して、子コンポーネントでその値を使ってチャートを描画する構成です。
結論だけ知りたい方は、こちら
まずは、リアクティブなデータを渡して描画してみる。
これがベースになります。<canvas>
の ctx
や Chart
オブジェクトの生成は mounted
の中でやらないといけないことと、
canvasRef
を return
しないといけないので注意。
<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>
<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
オブジェクトには反映されません...
<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
オブジェクトをリアクティブにしないといけないのか。それでおしまい!って思ったけど、なぜかチャートは更新されません。
<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
は更新されない。
<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>
<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 オブジェクトを生成するようにしたら変更が反映されました!なるほど!
<script>
... 中略 ...
setup(props) {
const date = computed(() => new Date(props.time));
return {date};
}
... 中略 ...
</script>
上と同じことをしてみる。
上と同じようにして、これでやっと解決。と思いきやこれでもだめという....
<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>
<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()
があることがわかり、
data
を watch()
で監視して、update()
で更新するという実装に落ち着きました。
https://misc.0o0o.org/chartjs-doc-ja/developers/updates.html
<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()
ってすれば、リアクティブになるんじゃないのかよーって感じです。
浅い知識で立ち向かってるので、なぜ狙い通りの挙動にならないのかわからないですし、
最終的にリアクティブにすることは諦めるということになったので、少々もやもやはあります。
もっとスマートな方法をご存知のかたいらっしゃったらコメントで教えていただけると幸いです。