39

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

8 Answers 8

51

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

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

Sign up to request clarification or add additional context in comments.

8 Comments

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
Can you explain the differences of your real situation with codepen?
` <line-chart v-if="type === "'line_chart'" :data="dataChart" :options="{responsive: true, maintainAspectRatio: false}"></line-chart>` 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
You make me realize the issue was actually caused by a v-if
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.
|
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 Comment

Adding deep may solve your issue watch: { chartData: { deep: true, handler() { this.renderChart(this.chartData, this.options); } } },
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

1 Comment

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.
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();
        })
    }
}

Comments

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

1 Comment

Unfortunately it did not make it. About the missing prop, as it is stated in the chart.js documentation, the mixin does the job.
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
    }
}

Comments

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"
/>

Comments

1

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.