Robert Robert - 4 months ago 23
Javascript Question

Chart is recreated using the old data

I use a JSF 2.2, a Primefaces 6.0, a CDI and a Highcharts 4.2.3. One of my page includes a chart (Highcharts) and a simple form which interferes in my chart. I've cut my code (look at below) to show you the most important part of my code.

I would like to achieve something like this:


  • When I press a
    commandButton
    in my form, the chart should recreate itself using new data.



So far I've:


  • declared several js variables which I use in my functions inside my script;

  • created a
    createChart
    function which downloads the data (for my chart) from a CDI bean (indeed from the database) using
    #{bean.getDataForChart1a(typeOfData)}
    and puts these data in the js variables. Apart from downloding the data, this function creates the chart as well;

  • created a
    createNewChart
    function which destroys my current chart and calls the
    createChart
    function to create new chart which should include new data.



The problem is the last stage. How you can see below I've added
oncomplete="createNewChart();"
attribute to my
commandButton
and at the moment my page works like this:


  • When I open my page, everything works. The data are downloaded and the chart is created.

  • When I press the
    commandButton
    , the chart is recreated but it uses the old data. I've noticed that the data isn't downloaded again in the
    createChart
    function, the js isn't executed my
    #{bean.getDataForChart1a(typeOfData)}
    . So, the
    #{bean.getDataForChart1a(typeOfData)}
    is executed only once at the beginning. I don't understand why.



How can I fix this issue?

My xhtml page:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:pt="http://xmlns.jcp.org/jsf/passthrough"
xmlns:pe="http://primefaces.org/ui/extensions">

<h:head>
<!-- loading css -->
</h:head>

<h:body>

<div id="container" style="height: 750px; width: 100%; margin: 0 auto;"/>

<script type="text/javascript">

//<![CDATA[

var chart;

var protos;
var dataChart;
var color;

var seriesChart;
var i, len;

function createChart() {

//Downloading data for the chart
protos = #{visualizationController.getDataForChart1a("protos")};
dataChart = #{visualizationController.getDataForChart1a("data")};
color = #{visualizationController.getDataForChart1a("color")};

seriesChart = [];

//Creating several series of the data
for (i = 0, len = protos.length; i < len; i++) {

seriesChart.push({
color: color[i],
name: protos[i],
data: dataChart[i]
//other options
});
}

console.time('scatter');
console.time('asyncRender');

Highcharts.setOptions({
lang: {
//translate
}
});

// Create the chart
chart = new Highcharts.Chart({

//other options

chart: {

//other options
renderTo: "container"
},

series : seriesChart
});

console.timeEnd('scatter');

}

function createNewChart(){

chart.destroy();
createChart();
}

createChart();

//]]>
</script>

<h:form id="filterForm">

<p:selectManyCheckbox id="filter" value="#{visualizationController.selectedKindOfNetwork}" layout="responsive" columns="4">
<p:ajax update="filterButton" />
<f:selectItems value="#{visualizationController.kindOfNetwork}" var="kindOfNetwork" itemLabel="#{kindOfNetwork}" itemValue="#{kindOfNetwork}" />
</p:selectManyCheckbox>

<br/>

<p:commandButton id="filterButton" value="Filtruj" action="#{visualizationController.actionFilterButtonForChart1a()}"
disabled="#{!visualizationController.visibilityFilterButtonForChart1a}"
update="filterForm"
oncomplete="createNewChart();"/>

</h:form>

</h:body>
</html>

Answer

I've just achieved the result which I expected. Based on the @SiMag's comments, I publish the answer which includes several possiable solutions. The post is divided into several sections which allow you to understand what I did wrong and what I did to fix that.


What did I do wrong?

As @SiMag wrote:

The script tag isn't updated after the first load so the #{visualizationController.getData...} expressions aren't evaluated anymore and they are referring to the old data.


What exactly was my problem?

At the moment, when I know what I did wrong, the correct questions are:

  • How to pass several values from the CDI bean to the javascript?
  • How to call the javascript function (passing the ready and required data to this function) on the server or client side?

How should my page work?

This is the last question to which you've to know answer to understand what I'll do in the next sections. My page should work like this:

  • When the page is loaded/opened, the createChart javascript function should be executed (automatically without any pressing the button or the link) and the function should use several values which I set in the CDI bean. As the result of this action, the chart should appear.
  • Each time when I press the button, the changeData javascript function should be executed and the function should use several values which I set in the CDI bean. As the result of this action, the chart should update its data which it displays.

If you would like to achieve something different, it isn't the problem. Based on the solutions which you can find here, you'll be able to achieve what you want.


First solution

The first solution is based on:

  • passing some values from the CDI bean to the javascript by means of the RequestContext. To achieve that I based on the primefaces documentation (chapter 11.1 RequestContext);
  • calling the createChart function on the server side;
  • calling the changeChart function on the client side.

First of all I've added three parameters to my javascript functions (one for each value which I want to pass to the javascript code from the CDI bean). At the moment, the script should look like this:

<script type="text/javascript">

    //<![CDATA[

    var chart;

    var protos;
    var dataChart;
    var color;

    var seriesChart;
    var i, len;

    //Creating the chart.
    function createChart(protosArg, dataArg, colorsArg) { 

        $(function() {

            //The data are parsed
            protos = JSON.parse(protosArg);
            dataChart = eval(dataArg);
            colors = JSON.parse(colorsArg);

            seriesChart = [];

            //Creating several series of the data
            for (i = 0, len = protos.length; i < len; i++) {

                seriesChart.push({
                    color: color[i],
                    name: protos[i],
                    data: dataChart[i]
                    //other options
                });
            }

            console.time('scatter');
            console.time('asyncRender');

            Highcharts.setOptions({
                lang: {
                    //translate
                }
            });

            // Create the chart        
            chart = new Highcharts.Chart({

                //other options

                chart: {

                    //other options                     
                    renderTo: "container"
                },                  

                series : seriesChart            
            }); 

            console.timeEnd('scatter');
        });
    }

    //Updating the chart using the new supplied data.
    function changeData(protosArg, dataArg, colorsArg){

        $(function() {

            protos = JSON.parse(protosArg);
            dataChart = eval(dataArg);
            colors = JSON.parse(colorsArg);

            seriesChart = [];

            //Creating several series of the data using the new supplied data.
            for (i = 0, len = protos.length; i < len; i++) {

                seriesChart.push({
                    color: color[i],
                    name: protos[i],
                    data: dataChart[i]
                    //other options
                });
            }

            //Removing the old data from the chart.
            for (i = 0, len = chart.series.length; i < len; i++) {

                chart.series[0].remove(false);
            }   

            //Inserting the new data to the chart.
            for (i = 0, len = protos.length; i < len; i++) {

                chart.addSeries(seriesChart[i],false);
            }

            chart.redraw();

        }); 
    }

    //]]>
</script>

Let's move on to the server. Here I've to show you, how my data look like. On the server side I've created three String variables. Output of this variables on the server side look like:

  • protos: ["tcp","udp",...]
  • data: [[[date,23],[date,1234]],[[date,1234]]]
  • color: ["black","white",...]

How you see above, I use to JSON.parse() and eval() methods to save the passed data. Why? Because the passed data are interpreted as the string by the browser, so we've to convert the data to the array.

On the server side I've also created several callback parameters using RequestContext#addCallbackParam() method. The getProtos(), getData() and getColors() methods return the string with my prepared data.

requestContext = RequestContext.getCurrentInstance();
requestContext.addCallbackParam("protosArg", getProtos());
requestContext.addCallbackParam("dataArg", getData());
requestContext.addCallbackParam("colorsArg", getColors());

Finally, I call RequestContext#execute() method which calls the createChart javascript function and passes three required values:

    @PostConstruct
    public void init() {

        //Creating the callback parameters.
        initData();     

        requestContext.execute(String.format("createChart('%s', '%s', '%s')", getProtos(),getData(),getColors()));
    }

Note that, I've used '' characters to achieve the correct representation on the javascript side. At the moment, the data will be interpreted as the string by the browser. I call execute() method in the init() method (which has the @PostConstruct annotation) of the CDI bean, so I'm sure that:

  • the createChart javascript function will be executed only once at the beginning (the CDI bean has @ViewScoped annotation);
  • the data which will be downloaded in the script, are ready.

The last thing which I've done on the client side is: change the value of the oncomplete attribute of the <p:commandButton> component. The oncomplete attribute calls changeData javascript function. The args parameter refers to the callback parameters which I've set on the server side.

<p:commandButton id="filterButton" value="Filter" action="#{chart2Controller.actionFilterButton()}"
                disabled="#{!chart2Controller.visibilityFilterButton}"
                update="filterForm"
                oncomplete="changeData(args.protosArg, args.dataArg, args.colorsArg);"/>

Part of my CDI bean:

@Named
@ViewScoped
public class Chart2Controller implements Serializable {

    /**
     * Start method.
     */
    @PostConstruct
    public void init() {

        //Creating the callback parameters.
        initData();     

        requestContext.execute(String.format("createChart('%s', '%s', '%s')", getProtos(),getData(),getColors()));
    }

    /**
     * Downloading the data from the database.
     */
    private void initData(){


        /*
         * Sending the query to the database including the filter options of the form,
         * saving the result and preparing the data basing on the result.
         */

        //Preparing the callback parameters.
        requestContext = RequestContext.getCurrentInstance();
        requestContext.addCallbackParam("protos", protos);
        requestContext.addCallbackParam("data", data);
        requestContext.addCallbackParam("colors", colors);
    }

    /**
     * Action for the filter button.
     */
    public void actionFilterButton(){

        initData(); 
    }   

    /**
     * Visibility for filter button.
     */
    public boolean getVisibilityFilterButton(){     
        //return true or false.
    }   

    //Getter and Setter

    private static final long serialVersionUID = -8128862377479499815L;

    @Inject
    private VisualizationService visualizationService;

    private RequestContext requestContext;

    private List<String> kindOfNetwork;
    private List<String> selectedKindOfNetwork;

    private String protos;
    private String data;
    private String colors;
}

Second solution

The second solution is based on:

  • passing some values from the CDI bean to the javascript by means of the RequestContext;
  • calling the createChart function on the client side;
  • calling the changeChart function on the client side.

How you see, this solution is similar to the previous solution but it's different place where the createChart function is calling. In this case, I've called the createChart on the client side and I've used the EL to supply the values from the CDI bean.

I'll not repeat the code, so I'll tell you only what I did to achieve the expected result. On the server side I've done the same things like in the previous solution except for calling the RequestContext#execute() method. Instead of that, I've called the createChart function on the client side at the end of the script. The script should like this:

<script type="text/javascript">

    //<![CDATA[

    var protos;
    var dataChart;
    var color;

    //other code


    //Creating the chart.
    function createChart(protosArg, dataArg, colorsArg) { 

        $(function() {

            protos = protosArg;
            dataChart = dataArg;
            colors = colorsArg;

            //other code
        });
    }

    //Updating the chart using the new supplied data.
    function changeData(protosArg, dataArg, colorsArg){

        $(function() {

            //The data are parsed
            protos = JSON.parse(protosArg);
            dataChart = eval(dataArg);
            colors = JSON.parse(colorsArg);

            //other code                
        }); 
    }

    createChart(#{chart2Controller.protos}, #{chart2Controller.data}, #{chart2Controller.colors});

    //]]>
</script>

Note that, I've also deleted JSON.parse() and eval() methods from the createChart function because the ELs have written directly into the javascript code and the browser correctly interprets the data as the array.


Third solution

The third solution is based on:

  • passing some values from the CDI bean to the javascript by means of the <h:inputHidden> componenets;
  • calling the createChart function on the client side;
  • calling the changeChart function on the client side.

This solution is quite different. On the beginning I've added three <h:inputHidden> componenets to my form (one for each value which I want to pass to the javascript). The form should look like this:

<h:form id="filterForm">

    <h:inputHidden id="protos" value="#{chart2Controller.protos}" />
    <h:inputHidden id="data" value="#{chart2Controller.data}" />
    <h:inputHidden id="colors" value="#{chart2Controller.colors}" />

    <p:selectManyCheckbox id="filter" value="#{chart2Controller.selectedKindOfNetwork}" layout="responsive" columns="4">
        <p:ajax update="filterButton" />
        <f:selectItems value="#{chart2Controller.kindOfNetwork}" var="kindOfNetwork" itemLabel="#{kindOfNetwork}" itemValue="#{kindOfNetwork}" />
    </p:selectManyCheckbox>

    <br/>

    <p:commandButton id="filterButton" value="Filtruj" action="#{chart2Controller.actionFilterButton()}"
                    disabled="#{!chart2Controller.visibilityFilterButton}"
                    update="filterForm"
                    oncomplete="changeData();"/>                

</h:form>

How you see above, I've added the id and value attributes of the <h:inputHidden> and I've saved the required data in the value attributes.

Note that, when you press the button, the form is updated (look at the update attribute of the <p:commandButton> component), so I'm sure that the data in the value attributes of the inputs will be downloaded every time when I press the button.

Finally, I've referred to these values in the javascript code by means of document.getElementById("filterForm:idOfInput").value. Remember to use JSON.parse() or eval() because the data are interpreted as the string. The script should look like this:

<script type="text/javascript">

    //<![CDATA[

    var chart;

    var protos;
    var dataChart;
    var colors;

    function createChart() {    

        $(function() {  

            protos = JSON.parse(document.getElementById("filterForm:protos").value);    
            dataChart = eval(document.getElementById("filterForm:data").value);
            colors = JSON.parse(document.getElementById("filterForm:colors").value);

            //other code
        });      
    }

    function changeData(){

        $(function() {  

            protos = JSON.parse(document.getElementById("filterForm:protos").value);    
            dataChart = eval(document.getElementById("filterForm:data").value);
            colors = JSON.parse(document.getElementById("filterForm:colors").value);

            //other code
        });
    }

    //]]>
</script>

Highcharts and change the chart's data

In my case I don't know how many series of the data I'll get after each pressed the button, so it's possiable that I'll get different number of series each time. Highcharts API doesn't support this case. You can use combination of Chart.addSeries(), Series.setData() and Series.remove() methods in loop for. If I've a little more time, I'll do that and I'll update this answer. The below you can find some ways to recreate/update the chart.

If you don't know the number of the series of the data.

First solution

If you don't know how many series of the data you'll get (like in my case) you can remove current series of the data using Series.remove() and then add new series of the data by means of Chart.addSeries():

function changeData(...){

    //Preparation of the received data.

    //Removing the old data from the chart.
    for (i = 0, len = chart.series.length; i < len; i++) {

        chart.series[0].remove(false);
    }   

    //Inserting the new data to the chart.
    for (i = 0, len = protos.length; i < len; i++) {

        chart.addSeries(seriesChart[i],false);
    }

    chart.redraw(); 
}

Note that, I've set false for the redraw parameters due to efficient. The Highcharts API says:

If doing more operations on the chart, it is a good idea to set redraw to false and call chart.redraw() after.

Second solution

You can also destroy the chart and recreate it. In this case you've to also change the changeData javascript function like this:

function changeData(...){

    chart.destroy();
    createChart(...);
}

If you know the number of the series of the data.

This case is easier than the previous one. Look at the code below, it doesn't need any explanation:

function changeData(...){

    //Preparation of the received data.

    for(var i = 0; i < chart.series.length; i++){

        chart.series[i].setData(seriesData[i],false);
    }

    chart.redraw();
}