apertureless / vue-chartjs

📊 Vue.js wrapper for Chart.js
https://vue-chartjs.org
MIT License
5.49k stars 832 forks source link

[Bug]: Can't get chart instance with composition api #1012

Open tonylee00111 opened 1 year ago

tonylee00111 commented 1 year ago

Would you like to work on a fix?

Current and expected behavior

Can't get chart instance by ref. Chart ref value always return null

This is my code

<template>
  <Bar ref="myChart" :data="chartData" :options="chartOptions" />
</template>

<script>
import {
  Chart as ChartJS,
  Title,
  Tooltip,
  Legend,
  BarElement,
  CategoryScale,
  LinearScale,
} from 'chart.js';
import { defineComponent, computed, onMounted, ref } from 'vue';
import { Bar } from 'vue-chartjs';
import * as chartConfig from './chartConfig.js';

ChartJS.register(
  CategoryScale,
  LinearScale,
  BarElement,
  Title,
  Tooltip,
  Legend
);

export default defineComponent({
  setup(props) {
    const myChart = ref(null);
    const chartData = ref(chartConfig.data);
    const chartOptions = ref(chartConfig.options);

    onMounted(() => {
      console.log(myChart.value.chart);  <-- chart will be null 
    });

    return {
      myChart,
      chartData,
      chartOptions,
    };
  },
  components: {
    Bar,
  },
});
</script>

Reproduction

https://stackblitz.com/edit/github-62t8lr?file=src/App.vue

chart.js version

^4.0.0

vue-chartjs version

^5.0.0

Possible solution

No response

DRoet commented 1 year ago

I'm seeing the same thing when using Vue 2.7 and this.$refs.refname.chart. the object key exists but is always null

apertureless commented 1 year ago

Hm thats weird. Because if you console.log myChart.value it shows the chart attribute. I will look into it.

image

palsingh commented 1 year ago

It seems that chart is not available when onMounted is called. If you use nextTick, then you can access the chart object.

onMounted(async () => {
    await nextTick()
    console.log('test: ', mychart.value.chart);
})
dnkmdg commented 1 year ago

I'm seeing the same behavior when running in <script setup>. When logging the chart ref in onExportImageClick it's always undefined.

EDIT: I found a way seconds after posting. So the ref isn't bound the same way in Composition as it would be in Options API, but accessing the ref and instead looking at chartInstance solved my problem. Code updated with working example:

<template>
    <div class="relative ">
        <PieChart
            ref="chart"
            :chart-data="chartData"
            :options="chartOptions"
            :height="height"
            :css-classes="cssClasses"
        />

        <div class="absolute top-0 right-0 px-2 py-1 space-x-2">
            <OutlineButton size="xs" class="py-1.5 !px-1.5 !text-base">
                <Fa :icon="['far','file-csv']" fixed-width />
            </OutlineButton>
            <OutlineButton
                size="xs"
                class="py-1.5 !px-1.5 !text-base"
                @click="onExportImageClick"
            >
                <Fa :icon="['far','file-image']" fixed-width />
            </OutlineButton>
        </div>
    </div>
</template>

<script setup>
import { Chart, registerables } from 'chart.js'
import { computed, nextTick, ref } from 'vue'
import { PieChart } from 'vue-chart-3'
import { emptyChartPlugin } from '@/Modules/Charts/ChartHelpers'
import OutlineButton from '../OutlineButton.vue'

Chart.defaults.font.family = 'Inter'
Chart.register(...registerables, emptyChartPlugin)

const props = defineProps({
    options: {
        type: Object,
        default: null,
    },
    data: {
        type: Object, 
        default: null,
    },
    height: {
        type: Number,
        default: 400,
    },
    cssClasses: {
        type: String, 
        default: '',
    },
})

const chart = ref(null)

const chartData = computed(() => {
    return props.data
})
const chartOptions = computed(() => {
    return props.options
})

const onExportImageClick = () => {
        //Correctly logs Chart instance for interaction
        console.log(chart.value.chartInstance)
}

</script>
palsingh commented 1 year ago

Isn't the property name chart instead of chartInstance? Ex: chart.value.chart

dnkmdg commented 1 year ago

Isn't the property name chart instead of chartInstance? Ex: chart.value.chart

No, for some reason it isn't reflected like that when using Composition API.

Rednas83 commented 1 year ago

@dnkmdg You are using vue-chart3 which is no longer maintained! Recommended is to use vue-chartjs.

I just started with migrating. from vue-chart3 Is there also a composition example preferably in typescript?

Something like:

<script setup lang="ts">
import { Line, Pie,  } from "vue-chartjs"
</script>
tpfau commented 1 year ago

I'm seeing the same situation in the options api. I wanted to listen to clicks on the points of a scatter plot, and thus wanted to access the chart. But during mounted() the chart property of the ref returns as null. More precisely, it returns as a proxy with this structure in the dev-tools:

Proxy { <target>: Proxy, <handler>: {…} }
  <target>: Proxy { <target>: {…}, <handler>: {…} }
    <target>: Object { chart: {…}, … }
      __v_skip: true
      chart: Object { __v_isShallow: true, dep: undefined, __v_isRef: true, … }
      ​​​<prototype>: Object { … }
    ​​<handler>: Object { get: get(target, key, receiver), set: set(target, key, value, receiver)
    }
  ​<handler>: Object { get: get(target, key), has: has(target, key) }

And when trying to access the chart property, that property (at least during mounted the chart is null.

I'm actually happy for any hints as to how to listen to clicks on the chart by other means.

quilkin commented 9 months ago

This is working for 'onHover' event, using vue-chartjs / vue3 / compostion API:

const chart = ref(null) ;
....
....
onHover: (e) => {
            if (chart.value === null)
                return;
            const thisChart = chart.value.chart;
            const canvasPosition = getRelativePosition(e, thisChart);
            const dataX = thisChart.scales.x.getValueForPixel(canvasPosition.x);
            const dataY = thisChart.scales.y.getValueForPixel(canvasPosition.y);
            console.log('x: ' + dataX + ' y: ' + dataY)
        }
<template>
    <Line
        ref="chart"
        :data = "chartData"
        :options = "chartOptions"
    />
</template>

It's giving a typescript error for `chart.value.chart' because I cannot find the correct type definition for the chart isntance, but the code runs OK.

jeroenpelgrims commented 8 months ago

@quilkin

It's giving a typescript error for `chart.value.chart' because I cannot find the correct type definition for the chart isntance, but the code runs OK.

import { ChartComponentRef } from "vue-chartjs";
...
const chart = ref<ChartComponentRef | null>(null)

or if you need more strict typing based on the chart type:

const chart = ref<ChartComponentRef<"line"> | null>(null)

@Rednas83

I just started with migrating. from vue-chart3 Is there also a composition example preferably in typescript?

For me the following works using @palsingh's workaround:

<script setup lang="ts">
import { nextTick, onMounted, ref } from "vue";
import { Line } from 'vue-chartjs'
import { ChartComponentRef } from "vue-chartjs";

const chart = ref<ChartComponentRef | null>(null)

onMounted(async () => {
    await nextTick()
    console.log(chart.value?.chart)
})
</script>

<template>
    <Line ref="chart" :data="<data>" :options="<options>" />
</template>
quilkin commented 8 months ago

@jeroenpelgrims Thanks for suggestion - ChartComponentRef solved that error but now getRelativePosition is complaining:

Argument of type '{ readonly platform: { acquireContext: (canvas: HTMLCanvasElement, options?: CanvasRenderingContext2DSettings | undefined) => CanvasRenderingContext2D | null; ... 6 more ...; updateConfig: (config: ChartConfiguration<...> | ChartConfigurationCustomTypesPerDataset<...>) => void; }; ... 46 more ...; getContext: () => ...' is not assignable to parameter of type 'Chart'. Type 'null' is not assignable to type 'Chart'.ts(2345)

Rednas83 commented 5 months ago

@jeroenpelgrims Just tried the solution but it doesn't seem to work anymore😢

Because I am getting the folowing runtime error image

Only modified it a little by adding some data and options.

<script setup lang="ts">
import { nextTick, onMounted, ref } from "vue"
import { Line } from "vue-chartjs"
import type { ChartComponentRef } from "vue-chartjs"

const chart = ref<ChartComponentRef | null>(null)
const chartData = ref({
  labels: ["January", "February", "March"],
  datasets: [{ data: [40, 20, 12] }],
})
const chartOptions = ref({
  responsive: true,
})

onMounted(async () => {
  await nextTick()
  console.log(chart.value?.chart)
})
</script>

<template>
  <Line ref="chart" :data="chartData" :options="chartOptions" />
</template>

I am using the latest packages

------------------------------
- Operating System: Windows_NT
- Node Version:     v20.10.0
- Nuxt Version:     3.10.1
- CLI Version:      3.10.0
- Nitro Version:    2.8.1
- Package Manager:  pnpm@8.15.1
- Builder:          -
- User Config:      telemetry, vite, modules, build, routeRules, components, content, formkit, imports, css, devtools
- Runtime Modules:  @vueuse/nuxt@10.7.2, normalizedModule(), @nuxt/content@2.12.0, @pinia/nuxt@0.5.1, @nuxtjs/tailwindcss@6.11.3, nuxt-icon@0.6.8, @formkit/nuxt@1.5.5
- Build Modules:    -
------------------------------

Does anyone has a working solution for nuxt with the composition api?