Drill-down charting with Sencha GXT 3.1

My latest assignment at work was to use charting in GXT and build a widget that has the ability to render one chart and drill down into charts of greater detail when the user clicks on an object in the parent chart. To complicate matters, the data used to render the drill-down chart is variable depending on the parameters passed in through our REST API. Finally, I wanted to encapsulate all this behavior within a portlet that could be added to a portal container by the user. How did I solve this?

When I started out, I knew that there were already handlers that could be attached to a series within a chart, but as with all things Sencha, nothing is as easy as it seems at first glance. Furthermore, I was battling with Sencha’s currently buggy beta release of GXT 3.1, which led me more than once to waste several hours thinking that I was doing something wrong when in fact, there was a problem on Sencha’s end. At any rate, this is how I solved the problem:

      Step 1: Define a chart object whose store can be manipulated through a configuration

For the drill-down chart, I decided to implement my own object which takes an object called ChartConfig. My ChartConfig class is just an object containing various options on how to render the chart, including what optional parameters to pass into my REST call.

public class ChartConfig {

	public int height;
	public int width;
	public Boolean initiallyInvisible;
	public List<Color> seriesColors; //Color of series in order
	public HashMap<String, String> options;	//REST options

}

I then defined my custom chart object to take this configuration into account when rendering itself.

public class MyBarChart extends UpdateableChart implements IsWidget {	
	
	public RegistrationBarChart() {
		super();
		_model = new StackedBarChartModel();
	}
	
	public RegistrationBarChart(ChartConfig _config) {
		super(_config);
		_model = new StackedBarChartModel();
	}
	
    @Override
	public Widget asWidget() 
	{
		if (_chart == null) 
		{	
			try {
				buildChart();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		return _chart;
	}
}

buildChart() is simply a method that uses the configuration, instantiates a new Chart and sets the ListStore that's used to render the chart. Don't worry about the StackedBarChartModel(). This is a custom data model and adapter that I had to implement to convert from JSON (following google's datatable format here: https://developers.google.com/chart/interactive/docs/reference#dataparam) to a ListStore consumable by Sencha (an entirely different battle. To be described in a follow-up post).

In any case, UpdateableChart is an abstract class that executes the REST API call based on the ChartConfig and regenerates the ListStore. Since I figured that all of our charts would follow the same logic here, I made it an abstract class with the subclasses needing to implement only one method: buildSeries();

public abstract class UpdateableChart {
	/**
	 * Abstract Methods
	 */
	public abstract void buildSeries();

	/**
	 * Refreshes the current chart with parameters passed in through the ChartConfig
	 * These parameters, in the HashMap<String,String> options map, get serialized
	 * and appended to the base REST URL.  
	 * 
	 * If options == NULL, the default url is used.
	 * 
	 * Upon success, the function will get a newstore and will call
	 * 	updateStore(newStore)
	 * @param c
	 */
	public void refresh(ChartConfig c) {
		String url = (c == null || c.getOptions().isEmpty()) ? _model.getListUrl() : serializeOptions(_model.getListUrl(), c.getOptions());
		
		_config = c;	//update config upon refresh
		
		RequestBuilder restReq = new RequestBuilder(RequestBuilder.GET,
				URL.encode(url)); //Use your flavor of request builder
		
		restReq.sendRequest(null, new () {

			@Override
			public void onSuccess(Response response) {
				JsonListReader<ChartObjects, ChartObject> reader = _model.getListReader();
				ListLoadResult<ChartObject> charts = reader.read(null, response.getText());
				//ListLoadResult<ChartObject> charts = reader.read(null, _resources.jsonPieData().getText());
				List<ChartObject> chartList = charts.getData();
				
				try {
					ChartObject chartObject = chartList.get(0);	//right now, only render first chart
					ListStore<DataPoint> newStore = _model.getListStore(chartObject);
					updateStore(newStore);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
			
			@Override
			public void onFailure(RestError restError) {
				//logger.error("Rest error" + restError.getErrorMessage());
			}
		});
	}
}

In the method above, you can see that after the REST Call is made and the JSON response is parsed into a ChartObject (basically a schema defined to work with Sencha's JSONReader), I use the adapter to convert the ChartObject into a ListStore which is then used to update the store which renders the chart. Here's an example of an implementation for my bar chart of buildSeries().

	@Override
	public void buildSeries() {
		for (int x = 0; x < _chart.getSeries().size(); x++) {
			_chart.removeSeries(_chart.getSeries(x));
		}
		
		BarSeries<DataPoint> series = createStackedBar();
		_chart.addSeries(series);
		
		refreshChart();  //regenerates chart axes
		setVisible(true);  //turn visible - after REST call returns
	}

In a nutshell, the work flow is like this:

  • Create a model to represent a ListStore which is used in a chart.
  • Instantiate a chart with a dummy store, which models this ListStore
  • Implement buildSeries() which will take the new ListStore object (in the class) and use it to regenerate the series
  • Re-render the chart
  • This framework makes it so that any charts that we add to our portal are able to be updated simply by calling the “refresh(ChartConfig config)” method.

        Step Two: From your entry point, add a method that will rerender a chart based on some variable parameter

    In our portlet, we have a pie chart that contains drill-downs to bar charts depending on what slice you pick. Before binding the drill-down event to the actual pie chart, I just wanted to make sure that charts could rerender properly.

    I did this by creating an entry point to the refresh(ChartConfig config) method on my bar chart object. I added a selector to our portlet widget and bound it to a “toggleView()” method which basically just generates the ChartConfig based on the selected value and calls refresh on the correct chart. Notice that I set both charts to visible(false) so that they don’t appear to the user. When implementing the buildSeries() method for both my pie and bar charts, I actually make the chart visible after the rest call returns.

    
    private void toggleViews(SeriesSelectionEvent<DataPoint> event) {
    		displayParent = !displayParent;
    		
    		//Show Pie Chart
    		if (displayParent) {	
    			_pieChart.setVisible(false);
    			_pieChart.refresh(_pieChartConfig);
    			_barChart.setVisible(false);
    			_backButton.disable();
    		}
    		
    		//Show Bar Chart (Drill Down)
    		else {
    			_pieChart.setVisible(false);
    			_barChart.setVisible(false);  //set invisible until rest call loads, buildSeries() will make the chart visible again
    			
    			//refresh registration bar chart depending on what slice is selected
    			_barChartConfig = _barChart.getChartConfig();
    			_barChartConfig.setOption(ChartConfig.TYPE, event.getItem().get(ChartModel.COL_LABEL));
    			_barChartConfig.setOption(ChartConfig.FILTER, "registrationState(EQ)"+event.getItem().get(ChartModel.COL_LABEL));
    			_barChartConfig.setOption(ChartConfig.LIMIT, "10");
    			_barChartConfig.setOption(ChartConfig.ORDER_BY, "count");
    			_barChartConfig.setOption(ChartConfig.ORDER_DIR, "DESC");
    			
    			if (_barChartConfig.getOption(ChartConfig.TYPE).equalsIgnoreCase(pc.registered())) {
    			   List<Color> c = new ArrayList<Color> ();
    			   c.add(new RGB(148, 174, 10));
    			   _barChartConfig.setSeriesColors(c);
    			} else if (_barChartConfig.getOption(ChartConfig.TYPE).equalsIgnoreCase(pc.unregistered())) {
    			   List<Color> c = new ArrayList<Color> ();
    			   c.add(new RGB(17, 95, 166));
    			   _barChartConfig.setSeriesColors(c);
    			} else if (_barChartConfig.getOption(ChartConfig.TYPE).equalsIgnoreCase(pc.partially_registered())) {
    			   List<Color> c = new ArrayList<Color> ();
    			   c.add(new RGB(166, 17, 32));
    			   _barChartConfig.setSeriesColors(c);
    			}
    			_barChart.refresh(_barChartConfig);
    			_backButton.enable();
    		}
    	}
    

    Finally, the last step was to bind the handler to the actual slices of my pie chart. One thing to keep in mind here. Because the handler is bound to the series of the pie chart, I couldn’t rebuild the pie series otherwise the event handler would actually fall off. Thus, I knew that after the first time the series was built, I needed to bind the handler, then leave it up to the ListStore to regenerate the pie chart. To do this, when I instantiate the pie chart, I had to set an empty ListStore to the store to render the pie chart. I’ve also included my buildSeries implementation for the pie chart below so you can see that I don’t rebuild the series at all with the pie chart.

    public class MyPieChart extends UpdateableChart implements IsWidget {
    	
    	private PieSeries<DataPoint> _series;
    	
        @Override
    	public Widget asWidget() 
    	{
    		if (_chart == null) 
    		{	
    			try {
    				buildChart();
    			} catch (Exception e) {
    				e.printStackTrace();
    			}
    			
    	    	//refresh(_config);	//refresh the chart store	
    		}
    
    		return _chart;
    	}
    	
    	/**
    	 * 	Iterates over each series and add a line series for each
    	 *  This is done only once.
    	 * @throws Exception 
    	 */
        @Override
    	public void buildChart() throws Exception {
    		_chart = new Chart<DataPoint>();
    		_chart.setDefaultInsets(10);
    		_store = _model.getBlankStore(numSeries, 1);   //This is where I bind the blank store to the chart
    		_chart.bindStore(_store);
    		_chart.setShadowChart(false);
    		_chart.setAnimated(true);		
    		_chart.setVisible(!_config.getInitiallyInvisible());
    		
    	    _series = createPieSeries();   //Note that this is only created once.
    	    _series.addColor(slice1);
    	    _series.addColor(slice2);
    	    _series.addColor(slice3);
    		_chart.addSeries(_series);
    		
    		final Legend<DataPoint> legend = new Legend<DataPoint>();
    		legend.setPosition(Position.RIGHT);
    		legend.setItemHighlighting(true);
    		legend.setItemHiding(true);
    		legend.getBorderConfig().setStrokeWidth(0);
    		_chart.setLegend(legend);
    		
    
    	}
     
            @Override
    	public void buildSeries() {
    		//Only build the Series the first time so we don't lose the selection handler binding
    		if (_store.size() > 0)
    			setVisible(true);
    	}
    }
    
        Step 3: Bind the drill down event handler to the series of the parent chart

    In the portlet widget, which as described above has both a pieChart and a barChart, after instantiating the pieChart, I bound the SeriesSelectionHandler to the pieChart in order to enable the drilldown. I didn’t have to wait for the REST call return to have data either, because I created a PieSeries with my dummy ListStore. This part I’m not that proud of. Because I bind the SeriesSelectionHandler outside the actual MyPieChart object, the implementation violates encapsulation. I could have implemented the SeriesSelectionHandler directly in MyPieChart, but I didn’t want to do this because I felt it would confuse developers later on why a PieChart had a BarChart object in it. I thought it would also set a bad precedent for development because it would seem like we needed to nest objects within objects whenever we wanted to create a drilldown. My implementation, though perhaps not ideal, increased our flexibility because it allows me to create PieCharts that don’t necessarily have to have barChart objects in them and yet still maintain the drilldown capability. In the future, we could create a datamodel that contains a tree-like hierarchy for parent/child drilldowns, where the back button always rerendered the parent, and the drilldown always rendered the child clicked. Something to think about. In any case, here is the binding of the toggleView() method to the SeriesSelectionEvent.

    	@Override
    	public Widget asWidget() {
    _pieChart.addSeriesHandlers(new SeriesSelectionHandler<DataPoint> () {
    
    				@Override
    				public void onSeriesSelection(
    						SeriesSelectionEvent<DataPoint> event) {
    					toggleViews(event);
    				}
    				
    			});
    }
    

    That’s it for now! Let me know if you have any questions or comments. In a post to come, I’ll describe how I modeled the google datatables JSON format into a ChartObject and how it’s consumed by GXT charts.

    The end result:

    The top level pie chart
    The top level pie chart

    Drilling down by clicking on a pie slice.
    Drilling down by clicking on a pie slice.
    Advertisements

    Leave a Reply

    Fill in your details below or click an icon to log in:

    WordPress.com Logo

    You are commenting using your WordPress.com account. Log Out /  Change )

    Google photo

    You are commenting using your Google account. Log Out /  Change )

    Twitter picture

    You are commenting using your Twitter account. Log Out /  Change )

    Facebook photo

    You are commenting using your Facebook account. Log Out /  Change )

    Connecting to %s