Search code examples
chart.jsvelo

Chart.js on Wix, Velo - refreshes, not working when clicking on publish


I'm using this tutorial to draw charts using chart.js in Wix Valo

I only placed a new button that changes the chart data like :

if (!chart1 ) {chart1 = new ChartJSAPI($w('#CustomElement1'));}
    chart1.customization = data_chart.customization
    chart1.data = data_chart.data 
    }

When I clicked on the preview button the chart refreshed well, but if I published it I got this error (I'm a premium customer)

hart.js:5552 Uncaught Error: Canvas is already in use. Chart with ID '0' must be destroyed before the canvas with ID 'myChart' can be reused. 

How to solve this?

The all code

import { ChartJSAPI } from 'public/chart-api';
import { chartCustomization } from 'public/chart-customization';
import wixData from 'wix-data';

$w.onReady(async function () {
     chart(processDataForChart(results2))

    })







 
var results2 = [
    {
        trip_date: {
            value: "2021-01-01"
        },
        trips_count: 19595
    },
    {
        trip_date: {
            value: "2021-02-01"
        },
        trips_count: 19460
    },
    {
        trip_date: {
            value: "2021-03-01"
        },
        trips_count: 10909
    },
    {
        trip_date: {
            value: "2021-04-01"
        },
        trips_count: 35
    }
];


var results3 = [
    {
        trip_date: {
            value: "2021-01-01"
        },
        trips_count: 1955
    },
    {
        trip_date: {
            value: "2021-02-01"
        },
        trips_count: 1960
    },
    {
        trip_date: {
            value: "2021-03-01"
        },
        trips_count: 1009
    },
    {
        trip_date: {
            value: "2021-04-01"
        },
        trips_count: 3570
    }
];






// This function is determine which column will be X 

function getColumnName(jsonData) {
var columns_counter = 0
var columns = Object.keys(jsonData[0])


for (var i = 0; i < columns.length; i++) {
        // var value = jsonData[0][column];
    // console.log('', typeof (jsonData[0][columns[i]])

        if (jsonData[0][columns[i]].value != undefined) {
            console.log('getColumnName',columns[i])
            return columns[i];
        }

       
        
    columns_counter ++

    }

 console.log('getColumnName',columns[0])
columns[0]



}





function processDataForChart(jsonData) {

var columns = Object.keys(jsonData[0])
var dataArray = [];
var counter = 0
var labels = [];
var columns_counter = 0
var backgroundColor1 =  ["rgba(255, 99, 132, 0.2)","rgba(54, 162, 235, 0.2)","rgba(255, 206, 86, 0.2)","rgba(75, 192, 192, 0.2)","rgba(255, 159, 64, 1)"]
var borderColor1 = ["rgba(255, 99, 132, 1)", "rgba(54, 162, 235, 1)","rgba(255, 206, 86, 1)","rgba(75, 192, 192, 1)","rgba(153, 102, 255, 1)","rgba(255, 159, 64, 1)"]
var backgroundC = []
var borderC = []
var x_axis =  getColumnName(jsonData)


columns.forEach(function(column) {


 console.log('column',column )
 
 console.log('x_axis', x_axis )
if(column != x_axis  ){

var label = [];
var type=[];
var data = [];
var backgroundC = [];
var borderC = [];
var borderWidth = [];

// console.log('columns',columns)





backgroundC.push(backgroundColor1[columns_counter]);
borderC.push(borderColor1[columns_counter]);
type.push("bar")
borderWidth.push(1)

// columns_counter ++

jsonData.forEach(function(item) {

     if (counter < 5){


    //  console.log('item[column]',item[column])
     
    // if(item[column].value == undefined){

    data.push(item[column]);
    
    // ;


    // } else{
    
    



    // }
    
    


     }
        });
 
  dataArray.push({
            label: column,
            type: type,
            data: data,
            backgroundColor: backgroundC,
            borderColor: borderC,
            borderWidth: borderWidth
        });
 
        counter++; // Increment counter after processing a column

}

else{

jsonData.forEach(function(item) {
labels.push(item[column].value);

})

}


columns_counter ++


})


// var output = '"labels":'+ labels + ',"datasets":' +  dataArray
//  console.log('dataArray',dataArray)
// return  output


var data_charts = {
    
    data: {
        "labels": labels,
        "datasets": dataArray
    },
    
        customization: {
               
                "borderWidth": 1,
                
            
        }
    
};


return data_charts




}


var chart1

async function chart(data_chart){

//  if(chart1 == undefined){

if (!chart1 ) {chart1 = new ChartJSAPI($w('#CustomElement1'));}
chart1.customization = data_chart.customization
chart1.data = data_chart.data 
}



export function button6_click(event) {

 chart(processDataForChart(results3))


}

Solution

  • Here is a minimal set of changes to the velo chart.js tutorial referenced in the question, that would allow for the update of the chart. The tutorial consists in four files:

    • a Chart.JS, which is the frontend, i.e., the javascript of the web page;
    • a chart-api.js and a chart-customization.js which are part of the backend, and might be placed in the wix app's /public/ directory
    • a chart.js, which is the class defining the custom element displaying the chart; it might be placed in /public/custom-elements

    The changes are to be made to the two chart.js/Chart.JS files; there are no changes required for chart-api.js or chart-customization.js

    To illustrate the dynamic data version we'll suppose a button (button1) was added to the page,
    that has an click handler called button1_click. Also, there's a collection "ChartItems2", that has the same labels as "ChartItems", but different values for the data. On pressing button1, we want the chart to update displaying the data from "ChartItems2", that means the bars of the chart will have different lengths.

    The frontend

    The standard method to update the frontend is assign new data to the chart object (instance of ChartJSAPI) and call its .render() method. This means that we have to keep track of this object, through a global variable; Here's the updated Chart.JS file:

    import { ChartJSAPI } from 'public/chart-api';
    import { chartCustomization } from 'public/chart-customization';
    import wixData from 'wix-data';
    
    let chart = null; // ChartJSAPI instance, not chart.js instance
    
    $w.onReady(async function () {
        chart = new ChartJSAPI($w('#customElement1'));
        chart.customization = chartCustomization;
    
        const chartItems = await wixData
            .query("ChartItems")
            .find()
            .then(res => res.items);
    
        const labels = chartItems.map(item => item.label);
        const data = chartItems.map(item => Number(item.data));
        const backgroundColor = chartItems.map(item => item.backgroundColor);
        const borderColor = chartItems.map(item => item.borderColor);
    
        chart.data = {
            labels,
            datasets: [{ data, backgroundColor, borderColor }]
        };
    });
    
    export async function button1_click(event) {
        if(chart){
            const chartItems2 = await wixData
                .query("ChartItems2")
                .find()
                .then(res => res.items);
                
            const dataNew = chartItems2.map(item => Number(item.data));
            chart.data.datasets[0].data = dataNew;
            chart.render();
        } 
    }
    

    The custom element class

    The changes above are the only required modifications in "Preview" mode; however, for the "published" site, one gets the error Canvas is already in use. Chart with ID '0' must be destroyed before the canvas with ID 'myChart' can be reused., as reported in the question.

    To address this issue one has to make some adjustments to the chart.js custom element class. Here, one has to keep track of the chart.js instance, the result of new Chart(...); we assign this object to the property _chart of the ChartElem class; we initialize it with null in the constructor:

    import Chart from 'chart.js/auto';
    
    class ChartElem extends HTMLElement {
        constructor() {
            super();
            this._shadow = this.attachShadow({ mode: 'open' });
            this._root = document.createElement('canvas');
            this._root.setAttribute("id", "myChart");
            this._root.setAttribute("style", "width: 100%");
            this._shadow.appendChild(this._root);
    
            this._parent = document.querySelector("chart-elem");
            this._chart = null; // added
        }
        ........
    

    Then, in the render method, we verify first if this._chart is initialized and if it is, we destroy the current chart first:

        ........
        render() {
            if(this._chart){ // if chart exists 
                this._chart.destroy?.();
            }
            
            const ctx = this._shadow.getElementById('myChart').getContext('2d');
            this._chart = new Chart(ctx, {
                type: 'bar',
                data: this.chartData,
                options: {
                    legend: {
                        labels: {
                            boxWidth: 0,
                        }
                    },
                    scales: {
                        y: { // modified to make compatible with newer versions of chart.js
                            beginAtZero: true,
                        },
                    },
                },
            });
        }
    }
    

    Also note the change in the scales options, required to make the code compatible with newer versions of chart.js. There are no changes to the other method of the class ChartElem.

    Alternatively, we can use chart.js method update method; it has the advantage that it doesn't recreate the chart:

        ........
    
        render() {
            if(this._chart){  // if chart exists
                this._chart.data =  this.chartData;
                this._chart.update();
                return;
            }
            
            const ctx = this._shadow.getElementById('myChart').getContext('2d');
            this._chart = new Chart(ctx, {
            ...... the same as above        
        }
    

    If we know that only the values of the first (and only) dataset are changed, we can replace

    this._chart.data =  this.chartData;
    

    with

    this._chart.data.datasets[0].data =  this.chartData.datasets[0].data;
    

    which results in a smooth transition from the first chart view to the second, the bars being animated from their first position to the second. That happens if the y-axis doesn't have to be rescaled, which means that either the maximum y value is the same, or, the option options.scales.y.max is set to a value that is greater than both the maximum values (the one before and the one after the update).