Chart.js的Vue.js组件 - 关于AJAX重新加载的奇怪问题

时间:2016-10-28 13:52:37

标签: vue.js chart.js

我有一个Vue.js 2.0组件,它提供了Chart.js 2.0条形图,并使用过滤器选择框动态重新加载数据。

将点击事件添加到图表数据集后,我在使用过滤器重新加载数据时遇到了问题。首次加载页面时,我可以单击每个数据集并将信息记录到控制台。从选择框中选择过滤器并通过AJAX重新加载数据集后,单击数据集时会得到非常零星的结果。重新加载后的大部分时间我得到Uncaught TypeError: Cannot read property '_index' of undefined但有时候我会得到记录到控制台的2-4个数据集,除了其中一个在当前视图中不再存在。我的组件代码如下。如何在重新加载图表数据后修复点击事件?任何帮助表示赞赏。

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

}

2 个答案:

答案 0 :(得分:3)

我已经在这里发布了一个答案,指出了一些Vue.js使用问题。但我认为真正的问题是不同的,如下所述:

reload函数执行以下操作:

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

如果您注意到,第二行是this.load(),它以异步方式调用fetchData。当response出现时,您使用this.render(response.data)

呈现图表

让我们说你的AJAX数据需要400毫秒。所以你的图表只会在400毫秒后呈现。但是,this.chart已被销毁,只有在render函数内400毫秒后才会使用new Chart(...)重新创建。

但是,reload函数会立即调用this.setChartClickHandler并参考已被销毁的this.chart

要解决此问题,您只需在this.setChartClickHandler(..)功能内创建new Chart后致电render

编辑:对更新后问题的其他想法

你现在还有另一个问题:你的功能是在里面创建一个新的范围,你有this的绑定问题

您目前有以下内容:

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

相反,您需要将其更改为:

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

箭头功能可确保您不在内部创建新范围,并且函数内的this与Vue组件的this相同。

还有另一项功能需要进行相同的更改:

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

以上几行必须是:

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

答案 1 :(得分:0)

您正在尝试使用{{...}}(胡须)来绑定您的类和ID。 VueJS不允许你这样做。相反,您需要使用v-bind,如下所述:

目前模板中的第一行是:

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

您需要将其更改为:

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

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

最重要的是,您的画布ID可能未在您的组件中正确绑定。目前你有:

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

需要:

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

参考:https://vuejs.org/guide/syntax.html#Attributes

您可以使用v-bind:classv-bind:id之类的速记格式,而不是:class:id

在上述情况之后,您可能仍会遇到问题。 VueJS呈现DOM,但您的画布可能无法立即可用。在具有该ID的画布在DOM中可用之前,您需要等待稍微延迟。为此,您可以在调用 chart.js API之前使用Vue.nextTick()或简称this.$nextTick()

Vue.nextTick()的参考:the API docs for v-bind

修改:其他参考

上面的代码在技术上应该可行 - 它应该为您提供一个指定了ID的canvas元素。在nextTick()中,您应该能够获得<canvas>并将内容绘制到其中。

如果由于任何原因不起作用,您可能需要使用指令捕获<canvas>元素。这是Stackoverflow中最近的一个答案(我几天前发布的),它有一个jsFiddle示例:http://vuejs.org/api/#Vue-nextTick

在该答案的jsFiddle示例中,我在不使用<canvas>参数的情况下捕获了id元素。原因:组件应在多个位置重复使用,因此具有固定的id可能会失败该目的。如该示例所示,您可以尝试使用指令直接捕获<canvas>元素,这使您的组件可以完全重用。

直接引用该jsFiddle示例:https://stackoverflow.com/a/40178466/654825