Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rerender when data has changed #10

Closed
blomdahldaniel opened this issue Sep 19, 2016 · 40 comments
Closed

Rerender when data has changed #10

blomdahldaniel opened this issue Sep 19, 2016 · 40 comments

Comments

@blomdahldaniel
Copy link

blomdahldaniel commented Sep 19, 2016

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.

@apertureless
Copy link
Owner

apertureless commented Sep 19, 2016

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.

@blomdahldaniel
Copy link
Author

blomdahldaniel commented Sep 19, 2016

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!

@blomdahldaniel
Copy link
Author

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.

@apertureless
Copy link
Owner

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

@blomdahldaniel
Copy link
Author

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

@blomdahldaniel
Copy link
Author

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?

@apertureless
Copy link
Owner

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.

@cheneyang
Copy link

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!

@apertureless
Copy link
Owner

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

@beebase
Copy link

beebase commented Oct 18, 2016

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?

@apertureless
Copy link
Owner

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

@beebase
Copy link

beebase commented Oct 18, 2016

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

@apertureless
Copy link
Owner

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

@MissHoya
Copy link

MissHoya commented Jan 6, 2017

@beebase me too. Do you solve it?

@apertureless
Copy link
Owner

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

@beebase
Copy link

beebase commented Jan 10, 2017

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.

@MissHoya
Copy link

@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.

@beebase
Copy link

beebase commented Jan 14, 2017

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.

@apertureless
Copy link
Owner

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

@MissHoya
Copy link

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

@apertureless
Copy link
Owner

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

@MissHoya
Copy link

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

@vysakhvasanth
Copy link

vysakhvasanth commented Jan 20, 2017

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>

@apertureless
Copy link
Owner

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.

@vysakhvasanth
Copy link

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/

@Andkoo
Copy link

Andkoo commented Mar 24, 2017

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

@apertureless
Copy link
Owner

Nice! Thanks for the info.

@XavierAgostini
Copy link

@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?

@Andkoo
Copy link

Andkoo commented Mar 27, 2017

@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.

@XavierAgostini
Copy link

XavierAgostini commented Mar 27, 2017

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.

@Andkoo
Copy link

Andkoo commented Mar 27, 2017

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

@apertureless
Copy link
Owner

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

@Andkoo
Copy link

Andkoo commented Mar 27, 2017

@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.

@XavierAgostini
Copy link

@Andkoo thank you that worked perfectly!

@andrewmartin
Copy link

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.

@NicksonYap
Copy link

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.

@suhailqaddoura
Copy link

hmmm

@DimitarDechew
Copy link

DimitarDechew commented Feb 27, 2019

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?

@philippe-bourdeau
Copy link

philippe-bourdeau commented Mar 14, 2020

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

@hemant-rao
Copy link

I know it's bad solution but you can fixed this issue by toggle chart parent with true/false,
Create new method:
updateChart() { this.chartToggler = false; this.$nextTick(() => { this.chartToggler = true; if (this.$refs.chartRef && this.$refs.chartRef.chartInstance) { this.$refs.chartRef.chartInstance.update(); } if (this.$refs.barChart && this.$refs.barChart.chartInstance) { this.$refs.barChart.chartInstance.update(); } }); },
call this when value update in watch:
watch: { chartOptions: { handler() { this.updateChart(); }, deep: true, }, chartData: { handler() { this.updateChart(); }, deep: true, }, },

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests