Vue-chartjs: Rerender when data has changed

Created on 19 Sep 2016  ·  39Comments  ·  Source: apertureless/vue-chartjs

Hi! I am using the next branch.

I was wondering how I can make my charts reactive to the data that I enter through the :data property.

So, what should I do to re-render/update a graph?

Edit:
And I do know that the data has changed. I can see that through Vue Devtools. So the new data is reactiv to the inside of my chart component. But there graph does not update when the new data arrives.

Hacktoberfest ☂ feature-request ☕ discuss

Most helpful comment

just to let you know - this worked like a charm for me

import { Doughnut } from 'vue-chartjs'

export default Doughnut.extend({
  name: 'DoughnutChart',
  props: ['data', 'options'],
  mounted () {
    this.renderChart(this.data, this.options)
  },
  watch: {
    data: function () {
      this._chart.destroy()
      this.renderChart(this.data, this.options)
    }
  }
})

All 39 comments

Well the chart does not update itself if new data arrives.
Because you only pass the data to the chartjs render function which creates an instance and draw the chart on the canvas.

However chartjs have methods to update the chart.
http://www.chartjs.org/docs/#advanced-usage

From the docs:

myLineChart.data.datasets[0].data[2] = 50; // Would update the first dataset's value of 'March' to be 50
myLineChart.update(); // Calling update now animates the position of March from 90 to 50.

Inside your chart component you can access the chart object over this._chart.

So you can change the data this._chart.data.datasets[0].data[2] = 100 and then this._chart.update()

You could also make a small update helper function, which listen on an event or something.
Or if you don't work with events, you can simply add a vue watch on the data.

great! I think this is just what I needed. Didnt know how to access the chart object.

Will have a go at it and ad a watcher for the property.
I will write my solution here when Im done. :)

Thanks for a great package!

Okey, so this is how I solved it. If you have any thoughts of improvement just let me know!

Keep in mind that my use case was to be able to dynamically change the datasets/data of the graph.

Solution:

  • If a new dataset was added/removed, then the entire chart needs to be re-rendered.
  • If data is changed/added/removed in the existing datasets then the graph should be updated.

To check what datasets is present i created an array containing the labels of the oldData.datasets and newData.datasets. Then I compare the arrays by converting them to json strings.

// MyLineChart.js
import { Line } from 'vue-chartjs'

export default Line.extend({
  name: 'my-line-chart',
  props: ['data'],
  watch: {
    'data': {
      handler: function (newData, oldData) {
        let chart = this._chart

        let newDataLabels = newData.datasets.map((dataset) => {
          return dataset.label
        })

        let oldDataLabels = oldData.datasets.map((dataset) => {
          return dataset.label
        })
        if (JSON.stringify(newDataLabels) === JSON.stringify(oldDataLabels)) {
          newData.datasets.forEach(function (dataset, i) {
            chart.data.datasets[i].data = dataset.data
          })
          chart.data.labels = newData.labels
          chart.update()
        } else {
          this.renderChart(this.data, this.options)
        }
      },
      deep: true
    }
  },
  data () {
    return {
      options: {
         /* my specific data */
      }
    }
  },
  mounted () {
    this.renderChart(this.data, this.options)
  }
})

I think that this should be the default behavior for this plugin..? If you think so too just let me know and I'll make a PR.

Sorry for the late reply. Pretty busy right now.
I think it's a good idea to automatically update / re-render the chart if the data changes.
If you want, feel free to submit a PR ;)

Cool, I will take a look at it and send you a PR. 👍

hm.. I dont get anything when i enter the localhost:8080 after npm run dev. My terminal says its launched but I just get a white screeen... Do you know what might be up with that?

When I was looking at your code. It doesnt quite feel right to try to stick in a watcher and also add an optional data property. What do you think of me just adding a LiveDataLineExample.js an then maybe refer to that in the readme?

Oh yeah, because the entry point index.js contains only the module exports.
If you want to see the example charts, you need to import the app.vue and create a vue instance

import Vue from 'vue'
import App from './examples/App'

 /* eslint-disable no-new */
 new Vue({
   components: { App }
 }).$mount('#app')

If you want you can just add an example. I guess the best way to implement it, would be a shared mixin. Because the data prop, watcher and the logic for update or rerender would be the same for every chart.

There were then two possibilities: Add the mixin to all base-charts so they all have this behaviour per default, or what's even cooler I think, to let people decide if they want the realtime updates.

So they have to add the mixin to their own components.

Hello, I am using v1.1.3 and I try to update chart view with watch but I got this error,

Uncaught TypeError: e.controllers[o.type] is not a constructor vue-chartjs.js?bbc5:15
I can not find the cause because the vue-chartjs.js file was proguarded

so is there any way to fix this? or the new version is coming?
thx!

What excatly are you trying to do?
Do you have a jsfiddle to reproduce it?

So you can change the data this._chart.data.datasets[0].data[2] = 100 and then this._chart.update()

Does this also work with vue-chartjs 1.x ? If not, are there other ways to make a chart reactive in 1.x?

Yeah this should work with vue-chartjs 1.x, too.
You just need to watch the vue-chartjs api changes, which are minor. Like 'render()' and not 'renderChart()' like in the 2.x.

There are also two mixins in the develop branch

Which abstract this logic for a data properties if you want to use like an api to get your data and for component props, if you want to pass the data through props.

I think you can more or less copy paste this mixins into your local project, and just change the renderChart() to render(). And it should... work :D

Or you make it by yourself. Its not a complicated logic. Just need to check if the data has changed and trigger the this._chart.update() or render().

hm, I think I have an other issue. Whenever I change the data in the parent component, the watcher in the chart component isn't triggered.
I'll first make sure the watcher is working before moving on.
parent component code
img2363

chart component
image

Do you have the deep: true property?

From vue docs:

Option: deep

To also detect nested value changes inside Objects, you need to pass in deep: true in the options argument. Note that you don’t need to do so to listen for Array mutations.

Alternatively try:

 watch: {
    data: function (val, oldVal) {
      console.log('new: %s, old: %s', val, oldVal)
    }
}

@beebase me too. Do you solve it?

@MissHoya are you implemented the watcher yourself or are you using the provided mixins?

I'm using the "reactiveProp" mixin which works very well.

  1. create a Chart.js and make sure to add "reactiveProp" as a mixin
    img2506

  2. import the Chart.js into your parent component
    img2508

  3. define it as a component in your parent component
    image

4.define chartData in the data section of your parent component
image

  1. put the chart tag with the chartData property in the template section of your parent component
    img2509

Any changes in this.chartData will trigger your chart.

@apertureless I use vue.v1 and branch1.1,3,I don't provide mixins. My use is as follows:

1.create a vocabChart.js
6e6985e2-b6e1-4056-8069-2a4348977ad5

2.import this and define a component
image

3.define chartData in parent component
image

4.add tag in template section
image

5.I click and try to change chartData.datasets[0].label(Maybe this method is wrong). I see label has been changed in parent component with vue-tool, but error and no chart update.
image
image

at the same time, I have a question:
I add watch in parent component
image
then I change chartData.datasets[0].label, but error, execute many times
image

so I don't know where wrong. My English is so poor, please forgive me.

My first thought is that your watch is triggering itself in an endless loop.
Maybe put a breakpoint on your console.log and check newData against oldData after every loop.

You should try the mixin method. No need to build a watcher for your chartData.

@MissHoya
Try to remove the deep: true option from the watcher.

I try to remove the deep:true in parent component, and then no error about rangeerror, but no print console.log('watch 1 ...'), so chart.js also no execute watcher. maybe the way that I modify chartData is wrong?
image

Can you provide a minimal repo for reproduction? It's really hard to tell, with only seeing small pieces of code.

I set a repository in my github: https://github.com/MissHoya/vue-chart-example

Hi @apertureless ,

I am experiencing similar issues with the reactiveProp mixin. I am updating the chartData prop asyn via an api call, in the axios callback. This doesn't trigger the watch or update chartData on the component side. If I put a breakpoint on where it is rendering, this.chartData returns undefined, whereas this.options object is updated.

vue version: 2.1.10,
vue-chartjs version: 2.3.3

Here is the code snippets.

BarChart.js (component)

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

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

Parent file

import BarChart from '../components/BarChart'
import axios from 'axios'

new Vue({
    el: '#app',
    components: {
        BarChart,
    },
    data: function() {
        return {
            data: {
                labels: [],
                datasets: [{
                    label: 'Total expenditure',
                    data: [],
                    lineTension: 0.1,
                    backgroundColor: "rgba(255, 87, 34, 1.0)",
                    borderColor: "rgba(229, 57, 53,1.0)",
                    borderCapStyle: 'round',
                    borderDash: [],
                    borderWidth: 3,
                    borderDashOffset: 0.0,
                    borderJoinStyle: 'round',
                    pointBorderColor: "rgba(255, 87, 34, 1.0)",
                    pointBackgroundColor: "#fff",
                    pointBorderWidth: 5,
                    pointHoverRadius: 1,
                    pointHoverBackgroundColor: "rgba(75,192,192,1)",
                    pointHoverBorderColor: "rgba(220,220,220,1)",
                    pointHoverBorderWidth: 0,
                    pointRadius: 1,
                    pointHitRadius: 10
                }],
            },
            isError: false,
            isLoading: true,
            options: {
                scales: {
                    yAxes: [{
                        gridLines: {
                            display: true,
                            color: "rgba(220,220,220,1)",
                        },
                        ticks: {
                            beginAtZero: true
                        }
                    }],
                    xAxes: [{
                        barThickness : 10,
                    }],
                }
            },
        }
    },
    mounted() {
        var self = this;
        axios.get('api/simpleAPI').then(function(response) {
            var values = response.data.data;
            for (var item in values) {
                self.data.datasets[0].data.push(values[item]['value']);
                self.data.labels.push(values[item]['id']);
            }
            self.isLoading = false;
        }).catch(function(error) {
            console.log(error);
            self.isLoading = false;
            self.isError = true;
        });
    },
    methods: {},
});

Template

<div id="app" style="padding:1em;" v-cloak>
  <bar-chart :height="300" :chartData="data" :options="options" v-if="!isError"></bar-chart>
</div>

Well I am not sure, how the vue watcher reacts to async data. However I would not recommend using the mixin in this case. Because your data comes async and it could possibly overload the watcher.

I would simply try events. Then you can use a finish callback or smth. send and finish event and then update or rerender the chart.

I managed to update it by calling update on the component when the data is ready.
Vue ref attribute: https://vuejs.org/v2/api/#ref
jsfiddle example: https://jsfiddle.net/xmqgnbu3/1/

just to let you know - this worked like a charm for me

import { Doughnut } from 'vue-chartjs'

export default Doughnut.extend({
  name: 'DoughnutChart',
  props: ['data', 'options'],
  mounted () {
    this.renderChart(this.data, this.options)
  },
  watch: {
    data: function () {
      this._chart.destroy()
      this.renderChart(this.data, this.options)
    }
  }
})

Nice! Thanks for the info.

@Andkoo were you able to do that without getting a "Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders..." error?

@XavierAgostini You can't mutate a prop, or better to say - you can...but I don't recommend it.

What's expected:

You want to change some data, that are being sent from a parent component. You want to change them in a child component.

What's actually happening:

The child component receives some data from the parent component and changing them in the child component doesn't change them in the parent component, therefore whenever the parent component re-renders (let's say you change a route or something) the change to these data is lost.

What should you do instead:

Create a data property in the child component that references to the prop, that is used to pass data from parent component, then work with this data property like this:

data () {
  return {
    someDataProperty: this.propThatReceivesDataFromParent
  }
},
props: {
  propThatReceivesDataFromParent: Array
}

_(note that prop type Array can be anything you need, let's say Number or Object etc. instead)_

or alternatively you can mutate the parent component data through an event emitter, or directly in the parent component etc. There are multiple ways to accomplish this.

Please provide a reproduction link so I can look into it, I need to know how you want to mutate the prop.

Thanks @Andkoo I will try that tonight. Here is my repo https://github.com/XavierAgostini/vue-finance . I'm trying to pass an object as a prop in my TFSA.vue component to a bar graph component.

@XavierAgostini I got a 404 error. Is your repo public? Please provide a proper reproduction link.

The markdown link is wrong:
https://github.com/XavierAgostini/vue-finance

@apertureless ah thanks, my bad
@XavierAgostini pass data to your <bar-graph> through :data instead of :chart-data and then modify your barGraph.vue to the structure, which I suggested:

import { Bar } from 'vue-chartjs'

export default Bar.extend({
  name: 'MyAwesomeBarChart',
  props: ['data', 'options'],
  mounted () {
    this.renderChart(this.data, this.options)
  },
  watch: {
    data: function () {
      this._chart.destroy()
      this.renderChart(this.data, this.options)
    }
  }
})

no need to call a mixin, because I set my own watcher right inside the component to watch the changes to data prop. Also don't forget to provide a name property for this component, because (don't quote me on that, maybe different circumstances, I am not sure) if you would want to use <bar-graph> in a component that is a child of another component, you would get the "please provide a name property for recursive components" error.

@Andkoo thank you that worked perfectly!

After a few hours of frustration, I finally figured this out. I am creating a chart that has a variable number of labels and fields.

I abandoned the seemingly buggy mixin , and just added my own listeners.

Just in case anyone else has any confusion, if you're setting up following the examples, I ended up doing this:

Vue.component 'bar-graph',
  extends: VueChartJs.Bar
  props: [
    'chartInfo',
    'chartLabels',
  ]
  watch:
    chartInfo:
      handler: (to, from) ->
        this.$data._chart.destroy()
        this.renderChart(this.chartInfo, this.options)
    chartLabels:
      handler: (to, from) ->
        this.$data._chart.destroy()
        this.renderChart(this.chartInfo, this.options)
  mounted: ->
    chartOptions =
      responsive: true
      maintainAspectRatio: true
      tooltips:
        enabled: false
      scales:
        xAxes: [
          ticks:
            autoSkip: false,
            maxRotation: 0,
            minRotation: 90
        ]

    @renderChart this.chartInfo,
      chartOptions

You'll note that this.$data._chart is the reference found when you are extending the chart.

Ended up just using: this.$data._chart.update() or this._data._chart.update()

I just couldn't get reactiveData to work.
turns out this._chart does not exist, so this._chart.update() would not work.

hmmm

Is there any update on this issue? I need help too.

import LineChart from './LineChart.js'

export default {
  components: {
    LineChart
  },
  data () {
    return {
      transactions: {},
      options: {
        maintainAspectRatio: false,
        responsive: true,
        legend: {
          display: true
        },
        scales: {
          yAxes: [{
            ticks: {
              beginAtZero: true
            }
          }]
        }
      }
    }
  },
  mounted () {
    this.fillData()
  },
  methods: {
    fillData () {
      this.transactions = {
        labels: ["Януари", "Февруари", "Март", "Април", "Май", "Юни", "Юли", "Август", "Септември", "Октомври", "Ноември", "Декември"],
        datasets: [{
          label: 'Users',
          data: [12, 19, 3, 5, 2, 3, 20, 33, 23, 12, 33, 10],
          backgroundColor: 'rgba(66, 165, 245, 0.5)',
          borderColor: '#2196F3',
          borderWidth: 1
        }]
      }
    },
    updateChart (period) {
      console.log('Chart updated for period: ' + period);
      this.transactions = {
        labels: ["Януари", "Февруари", "Март", "Април", "Май", "Юни", "Юли", "Август", "Септември", "Октомври", "Ноември", "Декември"],
        datasets: [{
          label: 'Users',
          data: [12, 19, 3, 5, 2, 3, 20, 33, 23, 12, 33, 10],
          backgroundColor: 'rgba(66, 165, 245, 0.5)',
          borderColor: '#2196F3',
          borderWidth: 1
        }, {
          label: 'Users',
          data: [123, 19, 3, 5, 2, 3, 20, 33, 23, 12, 33, 10],
          backgroundColor: 'rgba(66, 165, 245, 0.5)',
          borderColor: '#2196F3',
          borderWidth: 1
        }]
      }
      console.log(this.transactions)
    }
  },
}

It works perfectly like this, but when I try to use this.transactions.datasets.push(object) It's not updating. Any ideas?

Out of context, but in case you stumble on this: be careful when you try to implement a watch handler.

I had an error mentioning this._chart does not exist; I was using an arrow function to define the handler, which cause problems. Use the following function form instead

  watch: {
    chartData: {
      handler: function () {
        this.$data._chart.destroy()
        this.renderChart(this.chartData, this.chartOptions)
      },
      deep: true,
      immediate: false
    }
  },
Was this page helpful?
0 / 5 - 0 ratings

Related issues

humanismusic picture humanismusic  ·  3Comments

DavidSotoA picture DavidSotoA  ·  3Comments

egorzekov picture egorzekov  ·  4Comments

timster picture timster  ·  5Comments

sylvaincaillot picture sylvaincaillot  ·  3Comments