27
votes

I'm using Vue.js and Chart.js to draw some charts. Each time I call the function generateChart(), the chart is not updated automatically. When I check the data in Vue Devtools, they are correct but the chart does not reflect the data. However, the chart does update when I resize the window.

  • What is wrong with what I'm doing?
  • How do I update the chart each time I call generateChart() ?

I feel this is going to be something related with object and array change detection caveats, but I'm not sure what to do.

https://codepen.io/anon/pen/bWRVKB?editors=1010

<template>
    <el-dialog title="Chart" v-model="showGeneratedChart">
        <line-chart :chartData="dataChart"></line-chart>
    </el-dialog>
</template>

<script>
export default {
    data() {
        const self = this;
        return {
            dataChart: {
                labels: [],
                datasets: [
                    {
                        label: "label",
                        backgroundColor: "#FC2525",
                        data: [0, 1, 2, 3, 4],
                    },
                ],
            },
        };
    },
    methods: {
        generateChart() {
            this.dataChart["labels"] = [];
            this.dataChart["datasets"] = [];

            // ... compute datasets and formattedLabels

            this.dataChart["labels"] = formattedLabels;
            this.dataChart["datasets"] = datasets;
        },
    },
};
</script>         

LineChart.js

import { Line, mixins } from 'vue-chartjs'

export default Line.extend({
    mixins: [mixins.reactiveProp],
    props: ["options"],
    mounted () {
        this.renderChart(this.chartData, this.options)
    }
})
5

5 Answers

42
votes

Use a computed property for the chart data. And instead of calling this.renderChart on watch wrap it in a method and reuse that method on mounted and in watch.

Vue.component("line-chart", {
  extends: VueChartJs.Line,
  props: ["data", "options"],
  mounted() {
    this.renderLineChart();
  },
  computed: {
    chartData: function() {
      return this.data;
    }
  },
  methods: {
    renderLineChart: function() {
    this.renderChart(
      {
        labels: [
          "January",
          "February",
          "March",
          "April",
          "May",
          "June",
          "July"
        ],
        datasets: [
          {
            label: "Data One",
            backgroundColor: "#f87979",
            data: this.chartData
          }
        ]
      },
      { responsive: true, maintainAspectRatio: false }
    );      
    }
  },
  watch: {
    data: function() {
      this._chart.destroy();
      //this.renderChart(this.data, this.options);
      this.renderLineChart();
    }
  }
});

var vm = new Vue({
  el: ".app",
  data: {
    message: "Hello World",
    dataChart: [10, 39, 10, 40, 39, 0, 0],
    test: [4, 4, 4, 4, 4, 4]
  },
  methods: {
    changeData: function() {
      this.dataChart = [6, 6, 3, 5, 5, 6];
    }
  }
});
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>Vue.jS Chart</title>
</head>
<body>
<div class="app">
    {{ dataChart }}
   <button v-on:click="changeData">Change data</button>
  <line-chart :data="dataChart" :options="{responsive: true, maintainAspectRatio: false}"></line-chart>
 
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.6/vue.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/vue-chartjs.full.min.js"></script>
</body>
</html>

You could also make the options a computed property, and if option not going to change much you can setup default props. https://vuejs.org/v2/guide/components.html#Prop-Validation

Here is a working codepen https://codepen.io/azs06/pen/KmqyaN?editors=1010

4
votes
watch: {
chartData: function (newData, oldData) {
  // let ctx = document.getElementById('doughnut-chart').getContext('2d')
  // console.log(ctx)
  // var chart = new Chart(ctx, {type: 'doughnut', data:, options: self.options})
  // // chart.config.data.datasets.push(newData)
  // chart.config.options.animation = false
  // console.log(chart)
  // chart.config.data.datasets.push(newData)
  // chart.config.optionsNoAnimation = optionsNoAnimation
  // console.log(chart.config.data.datasets.push(newData))
  // this.dataset = newData
  // chart.update()
  // console.log('options', this.data)
  console.log('new data from watcher', newData)
  this.data.datasets[0].data = newData
  this.renderChart(this.data, this.options)
}
}

add custom watcher to update any vue chart graph

4
votes

My solution is without mixins and using a watch to the prop.

watch: {
    chartData: function() {
        this.renderChart(this.chartData, this.options);
    }
  }

But, this don't work until I change the chartData in another component like this:

this.chartData = {
            labels: [],
            datasets: []
};
this.chartData.labels = labels;
this.chartData.datasets = datasets;

If I just replace the labels and datasets, the watch won't fired.

2
votes

I never used vue-chartjs before, but it looks like your only issue is that you forgot to explicitely receive chartData as a prop in your line-chart component:

Change

export default Line.extend({
    mixins: [mixins.reactiveProp],
    props: ["options"],
    mounted () {
        this.renderChart(this.chartData, this.options)
    }
})

with

export default Line.extend({
    mixins: [mixins.reactiveProp],
    props: ["chartData", "options"],
    mounted () {
        this.renderChart(this.chartData, this.options)
    }
})

Also, be aware of vue reactivity issues when changing objects, this won't work:

this.dataChart['datasets'] = datasets;

you have to do something like this:

Vue.set(this.dataChart, 'datasets', datasets);

in order Vue to detect changes in your object.

More info about reactivity: https://vuejs.org/v2/guide/reactivity.html

More info about reactivity in charts: http://vue-chartjs.org/#/home?id=reactive-data

1
votes

Your solution is actually nearly correct. You cannot modify the subproperties of the chart dataset directly. You must set the this.datachart object itself. The default mixin mixins.reactiveProp will automatically add a watcher to the component's chartData property. See the documentation here. This is why modification of the subproperties does not work without further code, see other answers.

generateChart() {
    // If you want to clear all chart data (not necessary)
    this.dataChart = {}

    // Compute datasets and formattedLabels
    let formattedLabels = ...
    let datasets = ...

    this.dataChart = {
        labels: formattedLabels,
        datasets: datasets
    }
}