Tapestry JFreeChart integration
Join the DZone community and get the full member experience.
Join For FreeI just finished integrating JFreeChart with Tapestry. Each time you integrate a library with Tapestry you are full of praise for the framework. This is something you can seldom say about other web frameworks.
I had two usages in mind.
- As a return value from an event handler.
- As a component which can be used to display JFreeChart and a corresponding imagemap
To display a chart only an instance of JFreeChart is not
enough, you need to know the width and height of the image too. Also
depending upon the type of image format you are going to use, additional
information is required e.g. in case of JPEG format quality.
So we create an abstract data model to hold the information at one place.
public abstract class ChartModel { private JFreeChart chart; private int width; private int height; private ChartRenderingInfo info; public ChartModel(JFreeChart chart, int width, int height) { this.chart = chart; this.width = width; this.height = height; info = new ChartRenderingInfo(new StandardEntityCollection()); } public JFreeChart getChart() { return chart; } public int getWidth() { return width; } public int getHeight() { return height; } public ChartRenderingInfo getInfo() { return info; } public abstract String getFormat(); public abstract String getContentType(); }
Then we create a concrete class each for a specific image format.
public class JPEGChartModel extends ChartModel { private float quality; public JPEGChartModel(JFreeChart chart, int width, int height, float quality) { super(chart, width, height); this.setQuality(quality); } @Override public String getFormat() { return "jpeg"; } @Override public String getContentType() { return "image/jpeg"; } public void setQuality(float quality) { this.quality = quality; } public float getQuality() { return quality; } } public class PNGChartModel extends ChartModel { private boolean encodeAlpha; private int compression; public PNGChartModel(JFreeChart chart, int width, int height, boolean encodeAlpha, int compression) { super(chart, width, height); this.setEncodeAlpha(encodeAlpha); this.setCompression(compression); } @Override public String getFormat() { return "png"; } @Override public String getContentType() { return "image/png"; } public void setEncodeAlpha(boolean encodeAlpha) { this.encodeAlpha = encodeAlpha; } public boolean isEncodeAlpha() { return encodeAlpha; } public void setCompression(int compression) { this.compression = compression; } public int getCompression() { return compression; } }
The image is written to output stream using a rendering service, ChartWriter.
public interface ChartWriter { public void writeChart(OutputStream out, ChartModel chartModel) throws IOException; } public class ChartWriterImpl implements ChartWriter { private ChartRenderer renderer; public ChartWriterImpl(ChartRenderer renderer) { this.renderer = renderer; } public void writeChart(OutputStream out, ChartModel chartModel) throws IOException { renderer.render(chartModel, chartModel.getChart(), out); } }
The implementation delegates the rendering to the ChartRenderer service which is a Strategy Service based on the class type
public interface ChartRenderer { void render(ChartModel chartModel, JFreeChart jfreeChart, OutputStream out) throws IOException; } //JPEG Chart Renderer public class JPEGChartRenderer implements ChartRenderer { public void render(ChartModel chartModel, JFreeChart jfreeChart, OutputStream out) throws IOException { JPEGChartModel jpegChart = (JPEGChartModel) chartModel; ChartUtilities.writeChartAsJPEG(out, jpegChart.getQuality(), jfreeChart, chartModel.getWidth(), chartModel.getHeight(), jpegChart.getInfo()); } } //PNG Chart Renderer public class PNGChartRenderer implements ChartRenderer { public void render(ChartModel chartModel, JFreeChart jfreeChart, OutputStream out) throws IOException { PNGChartModel pngChart = (PNGChartModel) chartModel; ChartUtilities.writeChartAsPNG(out, jfreeChart, chartModel.getWidth(), chartModel.getHeight(), pngChart.getInfo(), pngChart.isEncodeAlpha(), pngChart.getCompression() ); } }
Now that the bases are covered, let us move on to the two usages we had planned.
As a return value from an event handler.
This can be done by contributing a ComponentEventResultProcessor to handle ChartModel as a return value.
public class ChartResultProcessor implements ComponentEventResultProcessor<ChartModel> { public Response response; private ChartWriter chartWriter; public ChartResultProcessor(Response response, ChartWriter chartWriter) { this.response = response; this.chartWriter = chartWriter; } public void processResultValue(ChartModel chartModel) throws IOException { response.disableCompression(); OutputStream out = response.getOutputStream(chartModel.getContentType()); chartWriter.writeChart(out, chartModel); response.setHeader("Pragma", "no-cache"); response.setHeader("Cache-Control", "no-cache"); response.setDateHeader("Expires", 0); } }
The ChartResultProcessor delegates the chart rendering to ChartWriter.
A component to display JFreeChart and a corresponding imagemap
As different parameters are required for displaying the chart in different image formats we create an abstract class. Any abstract component class muct be placed in ${application-package}.base. The AbstractChart is implemented as
@Import(library = "chart.js") public abstract class AbstractChart implements ClientElement { @Parameter(value = "prop:componentResources.id", defaultPrefix = BindingConstants.LITERAL, allowNull = false) private String clientId; @Parameter(required = true, allowNull = false) private JFreeChart chart; @Parameter(required = true, defaultPrefix = BindingConstants.LITERAL, allowNull = false) private int width; @Parameter(required = true, defaultPrefix = BindingConstants.LITERAL, allowNull = false) private int height; @Parameter private Object[] context; @Parameter(defaultPrefix = BindingConstants.LITERAL) private String zone; @Parameter private ToolTipTagFragmentGenerator toolTipTagGenerator; @Parameter(value = "false", defaultPrefix = BindingConstants.LITERAL) private boolean useMap; ToolTipTagFragmentGenerator defaultToolTipTagGenerator() { return new StandardToolTipTagFragmentGenerator(); } @Inject private ComponentResources resources; @Inject private JavaScriptSupport javaScriptSupport; @Inject private ChartWriter chartWriter; private String assignedClientId; private ChartModel internalChart; void setupRender() { assignedClientId = javaScriptSupport.allocateClientId(clientId); } boolean beginRender(MarkupWriter writer) { // Outer Div writer.element("div", "id", getClientId()); // Write image tag writer.element("img", "src", getImageURL()); // Add map if required if(useMap) { writer.attributes("useMap", "#" + getMapName()); } writer.end(); // Close img tag String selectMapURL = getSelectMapURL(); if(useMap) { createChart(); initializeChart(); writer.writeRaw(ChartUtilities.getImageMap(getMapName(), internalChart.getInfo(), toolTipTagGenerator, getURLTagGenerator(selectMapURL))); } writer.end();// Close Outer Div if(zone != null) { addJavaScript(selectMapURL); } return false; } private String getSelectMapURL() { return resources.createEventLink(ChartConstants.SELECT_MAP, context).toAbsoluteURI(); } private URLTagFragmentGenerator getURLTagGenerator(final String url) { return new URLTagFragmentGenerator() { public String generateURLFragment(String text) { String[] parts = text.split("\\?"); return String.format("href='%s?%s'", url, parts[1]); } }; } private String getImageURL() { return resources.createEventLink(ChartConstants.SHOW_CHART, context).toAbsoluteURI(); } private String getMapName() { return getClientId() + "_map"; } public String getClientId() { return assignedClientId; } private void addJavaScript(String url) { JSONObject params = new JSONObject(); params.put("zone", zone); params.put("id", getMapName()); params.put("url", url); javaScriptSupport.addInitializerCall("mapToZone", params); } @OnEvent(ChartConstants.SHOW_CHART) Object showChart() { createChart(); return internalChart; } private void createChart() { internalChart = createChart(chart, width, height); } private void initializeChart() { OutputStream out = new DummyOutputStream(); try { chartWriter.writeChart(out, internalChart); } catch(IOException e) { throw new RuntimeException("Could not write chart : ", e); } } protected abstract ChartModel createChart(JFreeChart chart, int width, int height); }
There is one hack. The imagemap is generated by ChartRenderingInfo which can only be obtained after the chart has been created. So we have to create the chart twice, once during the rendering phase to obtain the map, and other during the action phase for rendering the chart. As the output is not required during the rendering phase, we use a DummyOutputStream.
public class DummyOutputStream extends OutputStream { @Override public void write(int b) throws IOException { } }
Another thing to notice is that the areas in imagemap have to be linked to the zone(if supplied). This is done by the script
Tapestry.Initializer.mapToZone = function(spec) { $A($(spec.id).childNodes).each(function(e) { if(e.tagName == "AREA") { Event.observe($(e), "click", function(event){ event.preventDefault(); var zoneManager = Tapestry.findZoneManagerForZone(zone); if(zoneManager != null) { zoneManager.updateFromURL(e.href); } }); } }); };
The JPEGChart & PNGChart inherits most of the functionality from the AbstractChart.
public class JPEGChart extends AbstractChart { @Parameter(value = "0.9", defaultPrefix = BindingConstants.LITERAL, allowNull = false) private float quality; @Override protected ChartModel createChart(JFreeChart chart, int width, int height) { return new JPEGChartModel(chart, width, height, quality); } } public class PNGChart extends AbstractChart { @Parameter(value = "false", defaultPrefix = BindingConstants.LITERAL, allowNull = false) private boolean encodeAlpha; @Parameter(value = "0", defaultPrefix = BindingConstants.LITERAL, allowNull = false) private int compression; @Override protected ChartModel createChart(JFreeChart chart, int width, int height) { return new PNGChartModel(chart, width, height, encodeAlpha, compression); } }
And finally the Module class
public class ChartModule { public static void bind(ServiceBinder binder) { binder.bind(ChartWriter.class, ChartWriterImpl.class); } public ChartRenderer buildChartRender(StrategyBuilder builder, @SuppressWarnings("rawtypes") Map<Class, ChartRenderer> chartRenderers) { return builder.build(ChartRenderer.class, chartRenderers); } @Contribute(ComponentEventResultProcessor.class) public void provideResultProcessors( @SuppressWarnings("rawtypes") MappedConfiguration<Class,ComponentEventResultProcessor> configuration) { configuration.addInstance(ChartModel.class, ChartResultProcessor.class); } @Contribute(ChartRenderer.class) public void provideChartRenderers( @SuppressWarnings("rawtypes") MappedConfiguration<Class, ChartRenderer> configuration) { configuration.addInstance(JPEGChartModel.class, JPEGChartRenderer.class); configuration.addInstance(PNGChartModel.class, PNGChartRenderer.class); } }
ChartConstants is implemented as
public class JPEGChart extends AbstractChart { @Parameter(value = "0.9", defaultPrefix = BindingConstants.LITERAL, allowNull = false) private float quality; @Override protected ChartModel createChart(JFreeChart chart, int width, int height) { return new JPEGChartModel(chart, width, height, quality); } }
The full source code is here
From http://tawus.wordpress.com/2011/07/30/tapestry-jfreechart-integration/
Opinions expressed by DZone contributors are their own.
Comments