Join the Stack Overflow Community
Stack Overflow is a community of 6.7 million programmers, just like you, helping each other.
Join them; it only takes a minute:
Sign up

I have a Vue.js 2.0 component that's providing a Chart.js 2.0 bar chart with dynamic reloading of data with a filter select box.

After adding in click events to the chart data sets I've run into an issue when reloading data with the filter. When the page first loads, I can click on each data set and log the information to the console. After I select a filter from the select box and the data sets reload via AJAX, I get really sporadic results when clicking the data sets. Most of the time after reloading I get Uncaught TypeError: Cannot read property '_index' of undefined but sometimes I will get between 2-4 data sets logged to the console, all but one of them no longer present in the current view. My component code is below. How can I fix the click events after reloading the chart data? Any help is appreciated.

import Chart from 'chart.js';

export default {
template: `
    <div v-bind:class="[chartDivSize]">
        <div class="card">
            <div class="header">
                <div class="row">
                    <div class="col-md-6">
                        <h4 class="title">
                            {{ title }}
                        </h4>
                        <p class="category">{{ category }}</p>
                    </div>
                    <div v-if="isManager" class="col-md-4 pull-right">
                        <h4 class="title pull-right">Filter by Team</h4>
                        <select v-model="selectedFilter" @change="reload" class="form-control selectpicker">
                            <option v-for="filter in filters">{{ filter }}</option>
                        </select>
                    </div>
                </div>
            </div>
                <div class="content">
                    <canvas v-bind:id="chartId" style="height: 352px; width: 704px;" width="1408" height="704"></canvas>
                </div>
                <div class="footer">
                    <div class="legend">
                    </div>
                    <hr>
                    <div class="stats">
                        <i class="fa fa-clock-o"></i>{{ stats }}
                    </div>
                </div>
            </div>
        </div>
    </div>
`,

props: ['ctx', 'chart', 'url', 'chartId', 'chartDivSize', 'title', 'category', 'stats', 'filters', 'isManager', 'selectedFilter', 'activePoints'],

ready() {
    this.load();
},

methods: {
    load() {
        this.fetchData().then(
            response => this.render(response.data)
        );
    },
    fetchData() {
        if(this.selectedFilter) {
            var resource = this.$resource(this.url);
            return resource.get({ filter: this.selectedFilter });
        } else {
            return this.$http.get(this.url);
        }
    },
    render(data) {
        this.title = data.title;
        this.category = data.category;
        this.stats = data.stats;
        this.filters = data.filters;
        this.filters.unshift('All');
        if(data.selectedFilter) {
            this.selectedFilter = data.selectedFilter;
        } else {
            this.selectedFilter = 'All';
        }
        this.isManager = data.isManager;

        this.ctx = $("#" + this.chartId);

        var chartData = {
            labels: data.labels,
            datasets: [
                {
                    label: data.metric,
                    data: data.data,
                    backgroundColor: data.background_colors,
                    hoverBackgroundColor: data.hover_colors
                }]
        };

        this.chart = new Chart(this.ctx, {
            type: "bar",
            data: chartData,
            options: {
                scaleLabel: {
                    display: true
                },
                scales: {
                    yAxes: [{
                        ticks: {
                            beginAtZero: true,
                            fontStyle: "bold"
                        }
                    }],
                    xAxes: [{
                        ticks: {
                            beginAtZero: true,
                            fontStyle: "bold"
                        }
                    }]
                },
                legend: {
                    display: false
                }
            }
        });

        this.$nextTick(() => {
            this.setChartClickHandler(this.ctx, this.chart);
        });

    },
    reload() {
        this.chart.destroy();
        this.load();
    },

    setChartClickHandler(ctx, chart) {
        ctx.on('click', evt => {
            var activePoints = chart.getElementsAtEvent(evt);
            var label = chart.data.labels[activePoints[0]._index];
            var value = chart.data.datasets[activePoints[0]._datasetIndex].data[activePoints[0]._index];
            console.log(label,value);
        });
    },
},

}

share|improve this question

I have already posted an answer here, which points to some Vue.js usage issues. But I think the real problem is different, as explained below:

You reload function does the following:

this.chart.destroy();
this.load();
this.setChartClickHandler(this.ctx, this.chart);

If you notice, the second line is this.load() which invokes fetchData asynchronously. When the response comes through, you render the chart using this.render(response.data)

Let's say your AJAX data takes 400 milli-seconds to come. So your chart will be rendered only after 400 milli-seconds. But your this.chart is already destroyed and will be recreated only after 400 milli-seconds inside your render function, using new Chart(...).

But your reload function immediately proceeds to call this.setChartClickHandler with reference to this.chart which is already destroyed.

To fix this, you need to call this.setChartClickHandler(..) only after the new Chart is created inside the render function.

EDIT: additional thoughts on the updated question

You have another problem now: Your function is creating a new scope inside, and you have problem with binding of this

You currently have the following:

this.$nextTick(function() {
    this.setChartClickHandler(this.ctx, this.chart);
});

Instead, you need to change it to:

this.$nextTick(() => {
    this.setChartClickHandler(this.ctx, this.chart);
});

The arrow function ensures that you do not create a new scope inside, and your this inside the function is same as this of Vue component.

There is another function where you need to make the same change:

ctx.on('click', function(evt) {
    var activePoints = chart.getElementsAtEvent(evt);
    var label = chart.data.labels[activePoints[0]._index];
    var value = chart.data.datasets[activePoints[0]._datasetIndex].data[activePoints[0]._index];
    console.log(label,value);
});

The above lines need to be:

ctx.on('click', evt => {
    var activePoints = chart.getElementsAtEvent(evt);
    var label = chart.data.labels[activePoints[0]._index];
    var value = chart.data.datasets[activePoints[0]._datasetIndex].data[activePoints[0]._index];
    console.log(label,value);
});
share|improve this answer
    
Hi @Mani, thank you for the help. I updated the component in my question based on your feedback, and although I'm getting the same results I figure I'm probably using the $nextTick incorrectly. I'll explore the docs on bindings as well. This is my first crack at Vue components. – lefty29er Oct 28 '16 at 16:14
2  
Your error is cannot read _index of undefined. So you need to concentrate your debugging efforts around activePoints[0]._index which is inside your setChartClickHandler function. I think you need to provide a callback from your load() function after AJAX is done. After that, your this.chart will be defined and your activePoints will have something in it. – Mani Oct 28 '16 at 16:17
    
I just noticed your edited code in question. Now you have a problem with binding of this. You need to start using arrow functions everywhere. Please check my edited answer above. – Mani Oct 28 '16 at 16:25
    
Ok, I updated again with the arrow functions, that makes sense but I'm getting the same results. The click events are really inconsistent after the reload. If I click on the first bar 10 times, it will return undefined. Then if I click on another bar it will return multiple data sets. If I go back to the first bar, it will start returning data sets, or not. It's all over the place. I was thinking that when setChartClickHandler() is called on nextTick(), all the activePoints would be re-initialized with the fresh data, but it does seem to contain data from the previous chart. – lefty29er Oct 28 '16 at 16:38
    
I am not able to find anything else that is missing in code, it looks ok to me. You need to look at the data returned from AJAX and the chart APIs. Can you use Vue devtools to check the data objects in component? – Mani Oct 28 '16 at 16:43

You are trying to use {{...}} (mustaches) for binding your classes and IDs. VueJS does not allow you to do that. Instead you need to use v-bind as explained below:

The first line in your template is currently:

<div class="{{ chartDivSize }}">

You need to change it to:

<div v-bind:class="[chartDivSize]">

Ref: https://vuejs.org/guide/class-and-style.html#Object-Syntax

Most importantly, your canvas ID is probably not getting binded properly in your component. Currently you have:

<canvas id="{{ chartId }}" style="height: 352px; width: 704px;" width="1408" height="704"></canvas>

It needs to be:

<canvas v-bind:id="chartId" style="height: 352px; width: 704px;" width="1408" height="704"></canvas>

Ref: https://vuejs.org/guide/syntax.html#Attributes

Instead of v-bind:class and v-bind:id, you may also use shorthand format like :class or :id, as specified in the API docs for v-bind.

You may still have issues after the above. VueJS renders the DOM, but your canvas may not be available instantaneously. You need to wait for a slight delay before the canvas with that ID becomes available in DOM. For that, you may use Vue.nextTick(), or simply this.$nextTick() before calling your chart.js APIs.

Reference for Vue.nextTick(): http://vuejs.org/api/#Vue-nextTick

Edit: Additional Reference

The above code should technically work - it should give you a canvas element with the ID specified. And in the nextTick(), you should be able to get that <canvas> and draw stuff into it.

If it does not work for any reason, you may need to capture the <canvas> element using directives. Here is a recent answer in Stackoverflow (that I posted few days ago), which has a jsFiddle example: http://stackoverflow.com/a/40178466/654825

In the jsFiddle example for that answer, I have captured the <canvas> element without using the id parameter. Reason: A component is meant to be reused in multiple places, and therefore having a fixed id may defeat that purpose. As seen in that example, you may attempt to capture the <canvas> element directly using directives, which makes your component fully reusable.

Direct reference to that jsFiddle example: https://jsfiddle.net/mani04/r4mbh6nu/

share|improve this answer
    
My above answer points to few Vue.js usage issues, but the real problem seems to be something else. I have posted another answer here, please check it out. – Mani Oct 28 '16 at 15:37

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.