3

After intensive search for the right utility for the job, I finally gave in and solved the problem I was facing: How do you render asynchronously loaded data (RPC) in a Google Visualization for GWT chart which itself is loaded via the asynchronous AjaxLoader doing the two requests in parallel?

Or, in other words: How can we make sure that chart rendering works regardless of whether chart data arrives before or after the Visualization library is loaded?

The two less restrictive (or; less ambitious) scenarios are pretty straightforward.

Easy scenario #1: Using static data

Without depending on RPC data using Visualization is straightforward. This is due to the fact that there is only one asynchronous request: loading the Visualization library. Therefore there is no risk that data will arrive before the Visualization library is loaded.

Sample code snippet:

public class MyViewImpl implements MyView {
    private Panel main;

    public MyViewImpl() {
        this.main = new FlowPanel();

        Runnable onVizLoaded = new Runnable() {
            @Override
            public void run() {
                DataTable data = DataTable.create();
                Options options = Options.create();

                // Populate data and options based on static data here...

                // Instantiate and attach new chart.
                LineChart lineChart = new LineChart(data, options);
                main.add(lineChart);
            }
        };
        // Load Google Visualization asynchronously.
        VisualizationUtils.loadVisualizationApi(onVizLoaded, LineChart.PACKAGE);
    }

    // View logic goes here...
}

The Visualization library is easily loaded and a LineChart is created and rendered when loading is done. Consider this outline of the load characteristics:

  Viz load: |---?---|
Chart data:         |
    Render:          |--->

Easy scenario #2: Running requests in serial

It is possible to utilize the above snippet when loading chart data via RPC. However the following implies that chart data will not be fetched until the Visualization library is loaded and ready. You may be OK with this performance impact - I am not!

  1. Mainly because I have no control over the load time of the Visualization library and thus no control over the delay before fetching data
  2. I don't like my View to dictate application behavior. Application logic belongs in the Presenter.

Sample code snippet:

public class MyViewImpl implements MyView {
    private Panel main;

    public MyViewImpl() {
        this.main = new FlowPanel();

        Runnable onVizLoaded = new Runnable() {
            @Override
            public void run() {
                // Make RPC call.
                ClientFactory.getService().getData(new AsyncCallback<MyResult>() {
                    @Override
                    public void onSuccess(MyResult result) {
                        DataTable data = DataTable.create();
                        Options options = Options.create();

                        // Populate data from RPC result.
                        data.addColumn(ColumnType.DATE);
                        data.addRow();
                        data.setValue(0, 0, result.getDate());
                        // ... Etc.

                        // Set options.
                        options.setWidth(500);
                        // ... Etc.

                        // Instantiate and attach new chart.
                        LineChart lineChart = new LineChart(data, options);
                        main.add(lineChart);
                    }

                    @Override
                    public void onFailure(Throwable caught) {
                        // Handle RPC error.
                    }
                });
            }
        };
        // Load Google Visualization asynchronously.
        VisualizationUtils.loadVisualizationApi(onVizLoaded, LineChart.PACKAGE);
    }

    // View logic goes here...
}

Load characteristics illustrates the problem:

  Viz load: |---?---|
Chart data:          |----~----|
    Render:                     |--->

Desired scenario: Running in parallel

The desired scenario will have the following charateristics with the Visualization library loading faster than data:

  Viz load: |---?---|
Chart data: |----~----|
    Render:            |--->

Or with data loading faster than the Visualization library:

  Viz load: |---?---|
Chart data: |--~--|
    Render:          |--->

The question is: How?

1 Answer 1

0

The following strategy will meet the requirements:

  1. Requests are run in parallel
  2. Charts are rendered immediately upon completion of both requests
  3. Application logic (RPC call) remains in the Presenter.

Sample code snippet: Presenter

public class MyPresenter implements MyView.Presenter {
    private MyView view;

    // ... Constructor etc. goes here...

    @Override
    public void render() {
        // Make RPC call immediately when Presenter should begin rendering.
        ClientFactory.getService().getData(new AsyncCallback<MyResult>() {
            @Override
            public void onSuccess(MyResult result) {
                // Pass data to view for rendering (when appropriate.)
                view.render(result);
            }

            @Override
            public void onFailure(Throwable caught) {
                // Handle RPC error.
            }
        });
    }
}

Sample code snippet: ViewImpl

public class MyViewImpl implements MyView {
    private Panel main;
    private AsyncChart<LineChart> asyncLineChart;

    public MyViewImpl() {
        this.main = new FlowPanel();

        // Runnable wrapper (see next snippet.)
        this.asyncLineChart = new AsyncChart<LineChart>() {
            @Override
            public LineChart onAttach(DataTable data, Options options) {
                // Instantiate and attach new chart.
                LineChart lineChart = new LineChart(data, options);
                main.add(lineChart);

                return lineChart;
            }
        };

        // Load Google Visualization asynchronously.
        VisualizationUtils.loadVisualizationApi(this.asyncLineChart, LineChart.PACKAGE);
    }

    @Override
    public void render(final MyResult result) { // Invoked from Presenter (see above snippet.)
        // Schedule rendering to be invoked ASAP (but not before Visualization library is loaded.)
        this.asyncLineChart.enqueueWriter(new AsyncChartWriter() {
            @Override
            public void onWrite(DataTable data, Options options) {
                // Populate data from RPC result.
                data.addColumn(ColumnType.DATE);
                data.addRow();
                data.setValue(0, 0, result.getDate());
                // ... Etc.

                // Set options.
                options.setWidth(500);
                // ... Etc.
            }
        });
    }
}

And now for the meat!

Sample code snippet: AsyncChart Ready to copy.

import com.google.gwt.visualization.client.DataTable;
import com.google.gwt.visualization.client.VisualizationUtils;
import com.google.gwt.visualization.client.visualizations.corechart.CoreChart;
import com.google.gwt.visualization.client.visualizations.corechart.Options;


/**
 * Wrapping {@link Runnable} to use with the load methods of  {@link VisualizationUtils} 
 * allowing a chart to be populated asynchronously (e.g., via RPC.)
 * 
 *  This wrapper handles the process of deferred attachment of the chart via the 
 *  {@link #onAttach(DataTable, Options)} method.
 *  
 *  Chart data is set / updated by means of a {@link AsyncChartWriter} mutating the 
 *  passed {@link DataTable} and {@link Options} instances.
 *  
 *  {@link AsyncChartWriter} can be reassigned (via {@link #enqueueWriter(AsyncChartWriter)}) 
 *  in order to redraw the chart.
 * 
 * @param <T>   The concrete chart type.
 */
public abstract class AsyncChart<T extends CoreChart> implements Runnable {
    public interface AsyncChartWriter {
        void onWrite(DataTable data, Options options);
    }

    private enum State {
        NEW,
        LOADED,
        ATTACHED
    }

    private State state;
    private T chart;
    private AsyncChartWriter enqueuedWriter;

    public AsyncChart() {
        this.state = State.NEW;
    }

    public abstract T onAttach(DataTable data, Options options);

    /**
     * Enqueues a writer to populate or manipulate the chart. This will happen immediately 
     * if the visualization API is already loaded and will otherwise be deferred.
     * 
     * @param writer    The writer to enqueue.
     */
    public void enqueueWriter(AsyncChartWriter writer) {
        this.enqueuedWriter = writer;
        processWriter();
    }

    /* (non-Javadoc)
     * @see java.lang.Runnable#run()
     * 
     * Invoked when the visualization API has loaded.
     */
    @Override
    public void run() {
        this.state = State.LOADED;
        if (this.enqueuedWriter != null) {
            processWriter();
        }
    }

    private void processWriter() {
        if (this.state == State.LOADED || this.state == State.ATTACHED) {
            DataTable data = DataTable.create();
            Options options = CoreChart.createOptions();
            this.enqueuedWriter.onWrite(data, options);
            if (this.state == State.LOADED) {
                this.chart = onAttach(data, options); // Instantiate and attach.
                this.state = State.ATTACHED;
            } else {
                this.chart.draw(data, options); // Redraw already attached chart.
            }
            this.enqueuedWriter = null;
        }
        // If state = NEW do nothing until run() is invoked (and state has changed to LOADED.) 
    }
}

Enjoy your parallel loads.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.