37

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)
    }
})
Cave Johnson
  • 6,499
  • 5
  • 38
  • 57
Léo Coco
  • 4,022
  • 11
  • 52
  • 97

8 Answers8

50

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/vue-chartjs@2.5.7-rc3/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://v2.vuejs.org/v2/guide/components.html#Prop-Validation

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

tony19
  • 125,647
  • 18
  • 229
  • 307
azs06
  • 3,467
  • 2
  • 21
  • 26
  • Works great with the codepen, but it does not work on my real situation. I guess it differs somehow. Yet I tries to reproduce the codepen as close as the real situation – Léo Coco May 02 '17 at 22:26
  • Can you explain the differences of your real situation with codepen? – azs06 May 03 '17 at 08:30
  • ` ` I forgot to mention that in my real situation there was a `v-if` on the chart component. Because of it, the component was just mounted once. I had to `reset` the `type` so the component is each time re-mounted so re-rendered – Léo Coco May 03 '17 at 13:08
  • You make me realize the issue was actually caused by a v-if – Léo Coco May 03 '17 at 13:08
  • Such a weird work around. Your codepen was invaluable nevertheless, thanks a zillion! I ended up altering the Bar chart component like so: https://gist.github.com/anonymous/19bb64e7d1b1813c1615ee4cde9c724c – Coreus Sep 14 '17 at 10:24
  • Glad it helped you. – azs06 Sep 14 '17 at 15:39
  • Thanks! This worked great. For my I had to use `this.$data._chart.destroy();`. (https://github.com/apertureless/vue-chartjs/issues/255) – albertski Feb 04 '18 at 17:02
  • 1
    Great solution. If you only ever change data, something like `this.$data._chart.data.datasets[0].data = this.data` combined with `this.$data._chart.update()` might suffice. This will only change the raw data and prevent flickering / redrawing the entire plot. – FirefoxMetzger Aug 10 '18 at 08:00
6

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.

  • 1
    Adding deep may solve your issue `watch: { chartData: { deep: true, handler() { this.renderChart(this.chartData, this.options); } } },` – Andi May 28 '20 at 13:54
4
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

Shrikant
  • 538
  • 5
  • 15
  • in my case I had to write: `this.chartdata = newData`. And when setting new data, I had to completely overwrite it. Overwriting just the datasets was not enough. – lenooh Apr 08 '20 at 23:15
2

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://v2.vuejs.org/v2/guide/reactivity.html

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

tony19
  • 125,647
  • 18
  • 229
  • 307
Gerardo Rosciano
  • 901
  • 5
  • 11
  • Unfortunately it did not make it. About the missing prop, as it is stated in the chart.js documentation, the mixin does the job. – Léo Coco May 02 '17 at 02:39
2

I simply re-rendered it on the nextTick without destroying and worked fine.

(vue 3, vue3-chart-v2:0.8.2)

mounted () {
    this.renderLineChart();
},
methods: {
    renderLineChart() {
        this.renderChart(this.chartData, this.chartOptions);
    }
},
watch: {
    chartData () {
        this.$nextTick(() => {
            this.renderLineChart();
        })
    }
}
Luke Snowden
  • 4,056
  • 2
  • 37
  • 70
1

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

For Vue 3 composition api a computed work well like this:

const chartData = computed(() => ({
  labels: [
    "Label 1",
    "Label 2"
  ],
  datasets: [
    { data: anyResponse.value.firstRowItems, label: 'Foo' },
    { data: anyResponse.value.secondRowItems, label: 'Bar' }
  ]
}));

And in your template:

<Line
  :chart-data="chartData"
/>
Hannes Be
  • 91
  • 1
  • 3
0

This approach maybe is not the best one but it is a practical one, I present it here, with the hope that it helps. Here I demonstrate the approach specifically for updating charts (vue-chartjs) but it can be used any time you have a problem with re-rendering (please read carefully through the code and comments it needs attentions):

  1. In your data() section add a property called re_render: true,
  2. In your methods section add an async function called forcedReRender as below:
async forcedReRender() {
   this.re_render = false;
}
  1. then where-ever you want to update your data do it like below:
this.forcedReRender().then(()=>{
   // update your chart data here without worrying about reactivity (objects, array, array objects, ...)
   this.re_render = true;
});
  1. at last in your template, using v-if="re_render" you can update your chart. (First it will be false for a glimpse and then true again)

That is it. Below I present the flow for pie chart

<template>
    // YOUR CODE 
    <div v-if="re_render">
        <div style="direction: ltr; width: -webkit-fill-available;">
            <Pie
                :data="chartDataComputed"
                :options="chartOptions"
            />
        </div>
    </div>
    // YOUR CODE
</template>
<script>
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js'
import { Pie } from 'vue-chartjs'

ChartJS.register(ArcElement, Tooltip, Legend)

export default {
    components: { Pie },
    data() {
        return {
            // YOUR PROPERTIES ...

            re_render: true, // USED FOR INITIALIZATION YOU CAN SET IT TO FALSE

            // THIS IS USED AS A STARTING POINT FOR COMPUTED FIELD chartDataComputed 
            chartData: {
                labels: ['GOLD'],
                datasets: [
                    {
                        backgroundColor: ['#DAA520'],
                        data: [75]
                    }
                ],
            },
            // A DESCENT DEFAULT CHART OPTION
            chartOptions: {
                responsive: true,
                maintainAspectRatio: false
            },
        }
    },
    computed: {
        /* THIS COMPUTED FIELD IS USED IN THE TEMPLATE
         * HERE, IT IS AN OVER KILL BUT YOU MAY USE IT
         * FOR YOUR OWN PURPOSES
        */
        chartDataComputed() {
            return this.chartData;
        },
    },
    methods: {
        // THIS ASYNC METHOD HELPS YOU WITH REACTIVITY
        async forcedReRender() {
            this.re_render = false;
        },
        addData() {
            this.forcedReRender().then(()=>{
                // ADDING DATE (WHAT EVER YOU NEED TO CALCULATE AND PUSH TO THE CHART)
                this.chartData.labels.push('SILVER');
                this.chartData.datasets[0].backgroundColor.push('#DDDDDD');
                this.chartData.datasets[0].data.push(25);
                // RE-RENDER THE CHART AGAIN
                this.re_render = true;
            });
        }
    },
}
</script>

Some notes:

  • Based on the official documentation Since v4 charts have data change watcher and options change watcher by default. Wrapper will update or re-render the chart if new data or new options is passed. Mixins have been removed. But it might not work perfectly so this answer is a second safe choice.

  • Again based on documentation, you should use computed fields. If it is not worked for you again the current approach is a safe choice.

  • You may use something like style="height: 350px" to preserve the needed space for the chart, so your content would be fixed when you re-render the chart.

  • below you may see the results in a real-world app: re-render a chart