3

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>
Robert
  • 762
  • 2
  • 10
  • 23
  • 1
    The script tag isn't updated after the first load so the statements #{visualizationController...} aren't evaluated anymore and they are referring to the old data. You could use three h:inputHidden within the form to store the data you need to access in JavaScript, and than get the value of these input in your createChart function – SiMag Jul 19 '16 at 21:18
  • as an addition to previous post I am not sure if creating new chart all the time is the best idea in your case. Couldn't you just use Series.update() or Series.setData() methods? – Grzegorz Blachliński Jul 20 '16 at 08:27
  • @SiMag, thanks for your feedback. I've tried to implement your idea. I've created three ` components (with the `id` and `value="#{visualizationController.getData...}"` attributes) in my `filterForm` form. Then, I've referred to the values of these inputs in my `createChart` function like this: `variable = document.getElementById("filterForm:idInput").value;`. Unfortunately, it isn't work. At the moment, my chart isn't created at all. It looks like: the Javascript code is executed before the form is created or before the data is downloaded or something like that. Is it possible? – Robert Jul 20 '16 at 15:36
  • @GrzegorzBlachliński, thanks for your advice. I'll try do that instead of recreate the chart, but first I've to find a way to redownload the new data from my bean and save these data in the js variables. – Robert Jul 20 '16 at 15:39
  • 1
    Try to change `createChart();` with `$(function(){ createChart(); });` so it's executed when the dom is ready – SiMag Jul 20 '16 at 16:49
  • @SiMag, thanks!, it seems that I'm close to the result which I expect. Now I've some problem with the downloaded data. When I called the `#{visualizationController.getData...}` expressions inside my `createChart` function, the downloaded data were interpreted correctly by Highcharts. Since I've called the `#{visualizationController.getData...}` expressions in `value` attribute of the `` inputs - the data haven't been interpreted correctly. Do I have to convert the data on the javascript side or to do something like that before I'll use these data in the chart? – Robert Jul 21 '16 at 10:19
  • My bean methods return string like these: `["a","b","c"]` or `[["x","y"],["z"]]` or other combinations. I've noticed (displaying the source code by means of the browser) that: in first case (calling the expressions in the js function) my data look exactly the same in source code of the browser; in second case (calling the expressions in `value` attribute of the inputs) my data look like this: `["a","b","c"]` in source code of the browser. I've tried to use `vars.replace(/"/g, '\\"')` in my `createChart` function but it isn't help, nothing change. – Robert Jul 21 '16 at 10:23
  • 1
    Yes, now the data retrieved by the value of inputHidden is a string, before it was written directly into the javascript code and the browser interpreted it as array. Now you need to convert the data to an array, try to see [here](http://stackoverflow.com/questions/13272406/javascript-string-to-array-conversion). By the way I suggest you to pass data from backing bean as json object/array and parse it in javascript. – SiMag Jul 21 '16 at 11:02
  • @SiMag, thanks a lot! At the moment everything works like I expect (using three `` inputs). Now I'm also trying achieve that by means of passing data from my bean to JS (using `RequestContext`) like you suggested. This way works as well, it seems the better idea, but the chart isn't created for the first time (when the page is loaded), the chart is created every time when I press the button. For the first time, the chart should be created without any pressing the button, but this issue deserves a separate thread, I think so. If I finish, I'll post answer with steps which I did. – Robert Jul 21 '16 at 16:26
  • @GrzegorzBlachliński, you suggested to use `Series.update()` or `Series.setData()` methods. How can I achieve that, if I don't know how many series of the data I'll get after each pressed the button? Is there any method which allows me to put several series of the data in one call or using loop for? I've read the Highcharts API and I've found there only this: `chart.series[0].setData()`. In this example I've to know how many series I'll get or I've to use combination of `Chart.addSeries()`, `Series.setData()` and `Series.remove` methods in some loop for (I think so), but it isn't look nice ;) – Robert Jul 21 '16 at 16:27
  • I've also tried use something like this `chart.series.setData(seriesChart)` (seriesChart includes from 0 to X series of the data). I had secretly hoped that it'll be worked but not. At the moment I use loop for to delete all my series of the data using `Series.remove` method and then I call the `Chart.addSeries` method to add all new series of the data in loop for. Does this way is more efficient than recreate the chart? – Robert Jul 21 '16 at 16:27
  • 1
    chart.series parameter contains array of all your series. If you will use Highcharts.each() method you will be able to iterate over all of your series and set the data for them 'in loop'. See this example: http://jsfiddle.net/ejxh3udm/ If you will have different number of series each time, that is true that sometimes you will need to add or remove series to your chart. – Grzegorz Blachliński Jul 22 '16 at 08:49
  • @GrzegorzBlachliński, thanks for your feedback. So, it'll be nice idea to supply support for the case in which we don't know the number of the series of the data. I'll suggest that [here](https://highcharts.uservoice.com/forums/55896-highcharts-javascript-api). BTW: I've just posted the answer with the solutions. – Robert Jul 22 '16 at 17:23

2 Answers2

0

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();
}
Robert
  • 762
  • 2
  • 10
  • 23
0

As an alternative, you can also use the highfaces JSF library which handles these cases for you including integration of the whole flow into the JSF flow.

So the solution would become as easy like:

<p:outputPanel id="something">
  <hf:chart type="line" value="# {lineChartBean.mappedList}" var="birth" subTitle="Including sub title"
 point="# {birth.amount}" tickLabel="# {birth.year}" title="Map with Lists of Pojos"/>
</p:outputPanel>
....
<p:commandButton id="filterButton" value="Filtruj" action="#{visualizationController.actionFilterButtonForChart1a()}"
                    disabled="#{!visualizationController.visibilityFilterButtonForChart1a}"
                    update="filterForm something"/>

HighFaces also works with dynamic number of series, you can check e.g. the Simple List, Map with POJO List example, and it of course nicely integrates with PrimeFaces.

mbauer
  • 183
  • 10