One of the most notable features of HTML5 is Canvas API which provides a rectangular region for custom drawing. There are several uses of it like: building games, image compositions, animations but this time we will see how to use it to render charts in web browser. Of course, we could write source code for drawing charts ourselves but it would be tedious and time-consuming task. Fortunately, there are several good JavaScript libraries which can perform this task for us.

One of them is jqPlot which we will use to render a very simple chart showing sales in given country in the current month:

Creating web page

Our only web page sales.jsp is pretty simple:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="s" uri="http://www.springframework.org/tags" %> <%@ taglib prefix="sf" uri="http://www.springframework.org/tags/form" %> <%@ page contentType="text/html" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Sales</title> <script type="text/javascript" src="<s:url value='/resources/libs/jquery/jquery.min.js' />" > </script> <script type="text/javascript" src="<s:url value='/resources/libs/jqplot/jquery.jqplot.min.js' />" > </script> <link href="<s:url value='/resources/libs/jqplot/jquery.jqplot.min.css'/>" rel="stylesheet" type="text/css" /> <script type="text/javascript" src="<s:url value='/resources/js/sales.js' />" > </script> <link href="<s:url value='/resources/css/styles.css'/>" rel="stylesheet" type="text/css" /> </head> <body> <form> <label for="countrySelect">Country:</label> <select id="countrySelect" > <c:forEach var="country" items="${countries}"> <option value="${country.code}">${country.name}</option> </c:forEach> </select> </form> <div id="chartError">Failed to contact server</div> <div id="chart"></div> </body> </html>

First, we include JQuery and jqPlot libraries which we are going to use later. Then we add our custom js/sales.js JavaScript file which contains logic to asynchronously update the web page once the country is changed (we will look at this file later) and css/styles.css CSS stylesheet:

#countrySelect { padding: 0.2em; } #chart { margin: 1em; width: 90%; height: auto; clear: both; } #chartError { margin: 1em; border-style: solid; border-width: medium; border-color: black; background-color: lightpink; font-weight: bold; padding: 0.5em; display: none; }

Finally, we add a drop-down menu with the list of all supported countries, a message to inform user about error (hidden by default) and a div element with identifier chart in which jqPlot will draw the chart. There are no canvas elements at the moment but they will be added later to the DOM tree by jqPlot.

Creating Spring MVC controller

The JSP web page retrieves the list of all supported countries from ${countries} model attribute which was populated by the following Spring controller:

package com.example.springrestchart; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller @RequestMapping(value = "/sales") public class SalesController { @Autowired private SalesProvider salesProvider; @RequestMapping(method = RequestMethod.GET) public String list(Model model) { List<Country> countries = salesProvider.getCountries(); model.addAttribute("countries", countries); return "sales"; } }

The controller uses injected instance of SalesProvider singleton bean:

package com.example.springrestchart; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.GregorianCalendar; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.annotation.PostConstruct; import org.springframework.stereotype.Component; @Component public class SalesProvider { private static final List<Country> COUNTRIES = Arrays.asList( new Country("UK", "United Kingdom"), new Country("DE", "Germany"), new Country("FR", "France")); private Map<String, Sales> sales; @PostConstruct protected void init() { sales = createSales(); } public List<Country> getCountries() { return Collections.unmodifiableList(COUNTRIES); } public Sales getSales(String countryCode) { Sales result = sales.get(countryCode); if (result != null) { return result; } else { throw new CountryNotFoundException("Country not found: " + countryCode); } } private static Map<String, Sales> createSales() { // omitted } private static Sales createRandomValues(Country country, int count) { // omitted } }

This provider is responsible for returning the list of all supported countries and their sales from the first day of the current month till today (one value per day). The actual sales values are auto-generated but the source code for this was omitted for clarity.

One important thing to notice is that getSales() method throws CountryNotFoundException exception if the sales for given country do not exist. The definitions of Country, Sales and CountryNotFoundException classes are straightforward:

Country:

package com.example.springrestchart; public class Country { private String code; private String name; public Country(String code, String name) { this.code = code; this.name = name; } public String getCode() { return code; } public String getName() { return name; } }

Sales:

package com.example.springrestchart; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class Sales { private Country country; private List<Double> values; public Sales(Country country, List<Double> values) { this.country = country; this.values = new ArrayList<>(values); } public Country getCountry() { return country; } public List<Double> getValues() { return Collections.unmodifiableList(values); } }

CountryNotFoundException:

package com.example.springrestchart; public class CountryNotFoundException extends RuntimeException { public CountryNotFoundException(String message) { super(message); } }

Creating Spring REST Controller

Now, we should have the web page display in web browser but nothing happens when country is changed. We are going to improve this by exposing the sales data using REST web service and later consuming it from web browser using AJAX and JQuery.

Let’s first look at the implementation of REST web service:

package com.example.springrestchart; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; @Controller public class SalesRestController { @Autowired private SalesProvider salesProvider; @RequestMapping(value = "/services/sales/{countryCode}", method = RequestMethod.GET, produces = "application/json") public @ResponseBody Sales getSales(@PathVariable("countryCode") String countryCode) { return salesProvider.getSales(countryCode); } @ResponseStatus(HttpStatus.NOT_FOUND) @ExceptionHandler(CountryNotFoundException.class) public void countryNotFound() { } }

It is a standard Spring controller with @Controller annotation. Alternatively, we could use RestController annotation and remove @ResponseBody annotation from getSales() method.

We inject the same SalesProvider as before to access sales data. Method getSales() of the controller is annotated with @RequestMapping to inform Spring that it should be invoked whenever HTTP GET request is sent to relative and parameterized URL services/sales/{countryCode} where {countryCode} should be replaced by one of the available country codes. In our case it would be UK, DE or FR.

Finally, we add @ResponseBody annotation to the method and specify produces element to indicate that the returned instance of Sales class should be automatically converted to JSON format before sending over HTTP. Spring uses Jackson library for this task so it must be added as a dependency to Maven POM file:

<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.4.1.1</version> </dependency>

If you have been following the article closely, you should remember that getSales() method in SalesProvider class throws CountryNotFoundException if the country code is unknown. Therefore, we install exception handler for it and inform Spring to return HTTP status 404 (Not Found) in case of the exception.

Implementing client-side logic

Finally, after all setup is done we can take a look at previously mentioned js/sales.js file:

function renderPlot(data) { $("#chart").empty(); $("#chartError").hide(); var countryName = data.country.name; var values = data.values; $.jqplot("chart", [values], { title: "<h2> Sales in " + countryName + " in current month</h2>", axes: { xaxis: { label: 'Day of month', pad: 0 }, yaxis: { label: 'Mln USD' } }}); } function renderError() { $("#chart").empty(); $("#chartError").show(); } function reloadPlot(countryCode) { $.ajax({ url: encodeURI("services/sales/" + countryCode), type: "GET", dataType: "json", success: renderPlot, error: renderError }); } function initializeCountries() { $("#countrySelect").change(function() { var countryCode = $(this).val(); reloadPlot(countryCode); }); reloadPlot($("#countrySelect").val()); } $(document).ready(initializeCountries);

Function initializeCountries(), which is called once the document is fully loaded, registers listener to be called when the country is changed. If it happens, the plot is reloaded to show the data for the chosen country.

Function reloadPlot() issues AJAX request to our REST service and passes country code as part of the URL path. If the request succeeds, renderPlot() function is called to render the chart with sales. Because the sales data was returned in JSON format, we can easily access it from JavaScript. We extract the country name and its sales (as an array of real numbers).

Finally, we call $.jqplot() function and pass it identifier of the div element where the chart should be rendered and the actual data. jqPlot can draw multiple data series on a single chart. We have only one series so we have to create an array with one element (our array with sales data) beforehand. jqPlot can take multiple options to customize appearance and behavior of the chart but for simplicity we specify axes labels and padding only.

Conclusion

jqPlot is versatile and very easy to use JavaScript library to render charts in web browser. The functionality used in this article is only a small portion of what it can do.

From user point of view charts generated by such library are more responsive and interactive than the charts generated on a server. It is for example possible to use highlighting, zooming, cursor tracking and even modify the values using drag-and-drop.

The complete source code for the example is available at GitHub.