XYDrawableAnnotation with JEditorPane to render HTML

A discussion forum for JFreeChart (a 2D chart library for the Java platform).
Locked
mickish
Posts: 29
Joined: Tue Jan 08, 2008 11:15 pm
Location: Venice, Florida

XYDrawableAnnotation with JEditorPane to render HTML

Post by mickish » Tue Mar 25, 2008 9:17 pm

My HTML-formatted annotations look great when they are displayed in a Java window, but do not appear in my chart's PDF output.

I have stepped through a lot of code to try to understand the difference between these situtations, and wrote a small example that illustrates my problem.

The program below is based on org.jfree.chart.demo.TimeSeriesChartDemo1. I've added some annotations, and print the chart to a PDF file (in your home directory) after displaying it in a Java window.

I would appreciate it if you could try this program and see if you can reproduce my results. I see 4 annotations in the Java window (yellow rectangle, black line, a green circle, and an HTML table), but only 3 annotations in the PDF file (the HTML table is missing).

Furthermore(!), if you click-and-drag (while viewing the PDF file in Acrobat) where the HTML annotation is supposed to be, you can select the invisible text! (See screenshot.) You can copy the "Hello" text into the cut buffer, and paste it into notepad. This tells me that the text is getting into the PDF file, but it is not visible.

I am rendering my HTML annotation in a JEditorPane, and adding it to the chart with an XYDrawableAnnotation. I implemented a method that creates an annotation for any JComponent, which seems to work fine for a JPanel with a custom paint() method. It does not seem to work for JEditorPane.

In writeChartAsPDF(), I have some commented-out code which will successfully draw a JEditorPane directly into the PDF file, bypassing the annotation system.

The fact that I can write a JEditorPane directly into the PDF, but not via the annotation system, suggests that there is more that I need to take into account when dealing with the annotation system.

Thanks for your advice.
Last edited by mickish on Tue Mar 25, 2008 9:21 pm, edited 1 time in total.

mickish
Posts: 29
Joined: Tue Jan 08, 2008 11:15 pm
Location: Venice, Florida

Post by mickish » Tue Mar 25, 2008 9:21 pm

Code: Select all

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;

import javax.swing.JComponent;
import javax.swing.JEditorPane;
import javax.swing.JPanel;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.Drawable;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.annotations.XYDrawableAnnotation;
import org.jfree.chart.annotations.XYLineAnnotation;
import org.jfree.chart.annotations.XYShapeAnnotation;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.chart.util.ApplicationFrame;
import org.jfree.chart.util.RectangleInsets;
import org.jfree.chart.util.RefineryUtilities;
import org.jfree.data.time.Month;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.data.xy.XYDataset;

import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Rectangle;
import com.lowagie.text.pdf.DefaultFontMapper;
import com.lowagie.text.pdf.FontMapper;
import com.lowagie.text.pdf.PdfContentByte;
import com.lowagie.text.pdf.PdfTemplate;
import com.lowagie.text.pdf.PdfWriter;

/**
 * An example of a time series chart.  For the most part, default settings are 
 * used, except that the renderer is modified to show filled shapes (as well as 
 * lines) at each data point.
 */
public class TimeSeriesChartDemoX extends ApplicationFrame {

	/**
	 * The chart displayed in this frame 
	 */
	final JFreeChart chart;
	
	/**
	 * Some sample HTML to display in an annotation
	 */
	final static String defaultHTML =
		"<body bgcolor='FFAAEE'>\n" +
		"	<table border='1' valign='center' align='center'>\n" +
		"	<tr><td><b>Hello</b></td><td>There</td></tr>\n" +
		"	<tr><td><font size='+3'>Hello</font></td><td><i>again</i></td></tr>\n" +
		"	</table>\n" +
		"</body>\n";
	
    /**
     * A demonstration application showing how to create a simple time series 
     * chart.  This example uses monthly data.
     *
     * @param title  the frame title.
     */
    public TimeSeriesChartDemoX(String title) {
        super(title);
        ChartPanel chartPanel = (ChartPanel) createDemoPanel();
        chartPanel.setPreferredSize(new java.awt.Dimension(500, 270));
        chartPanel.setMouseZoomable(true, false);
        setContentPane(chartPanel);
        
        this.chart = chartPanel.getChart();
    }

    /**
     * Creates a chart.
     * 
     * @param dataset  a dataset.
     * 
     * @return A chart.
     */
    private static JFreeChart createChart(XYDataset dataset) {

        JFreeChart chart = ChartFactory.createTimeSeriesChart(
            "Legal & General Unit Trust Prices",  // title
            "Date",             // x-axis label
            "Price Per Unit",   // y-axis label
            dataset,            // data
            true,               // create legend?
            true,               // generate tooltips?
            false               // generate URLs?
        );

        chart.setBackgroundPaint(Color.white);

        XYPlot plot = (XYPlot) chart.getPlot();
        plot.setBackgroundPaint(Color.lightGray);
        plot.setDomainGridlinePaint(Color.white);
        plot.setRangeGridlinePaint(Color.white);
        plot.setAxisOffset(new RectangleInsets(5.0, 5.0, 5.0, 5.0));
        plot.setDomainCrosshairVisible(true);
        plot.setRangeCrosshairVisible(true);
        
        XYItemRenderer r = plot.getRenderer();
        if (r instanceof XYLineAndShapeRenderer) {
            XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) r;
            renderer.setBaseShapesVisible(true);
            renderer.setBaseShapesFilled(true);
        }
        
        DateAxis axis = (DateAxis) plot.getDomainAxis();
        axis.setDateFormatOverride(new SimpleDateFormat("MMM-yyyy"));
        
        addAnnotations( chart );
        
        return chart;
    }
    
	//--------------------------------------------------
    
    private static void addAnnotations(JFreeChart chart) {
    	
    	long t1 = (new GregorianCalendar(2001, Calendar.MARCH,  17)).getTimeInMillis();
    	long t2 = (new GregorianCalendar(2001, Calendar.JUNE,   17)).getTimeInMillis();
    	long t3 = (new GregorianCalendar(2001, Calendar.AUGUST, 17)).getTimeInMillis();
    	long t4 = (new GregorianCalendar(2002, Calendar.MARCH,  17)).getTimeInMillis();
    	
    	// yellow rectangle
		chart.getXYPlot().addAnnotation(
	        new XYShapeAnnotation(
	            new Rectangle2D.Double(t1, 175, (t2-t1), 8), 
	            new BasicStroke(1.0f), Color.blue, Color.yellow ) );
		
		// black line 
		chart.getXYPlot().addAnnotation(
	        new XYLineAnnotation(t1, 130, t2, 160) );
		
		// JPanel that paints a circle
		chart.getXYPlot().addAnnotation(
				createXYDrawableAnnotation(
					new JPanel() {
						@Override
						public void paint(Graphics g) {
							g.setColor( java.awt.Color.green );
							g.fillOval(10, 10, 20, 20);
						}
					},
					t3, 170 ) );
		
		// XYDrawableAnnotation for a JEditorPane
		chart.getXYPlot().addAnnotation(
				createXYDrawableAnnotation(
					createHTMLEditorPane( defaultHTML, 150 ),
					t4, 140 ) );

    }
    
	//--------------------------------------------------
	
	/**
	 * Convenience method for creating XYDrawableAnnotations from JComponents.
	 */
	public static XYDrawableAnnotation createXYDrawableAnnotation(final JComponent c,
			final double x, final double y) {
		
		final Drawable d = new Drawable() {
			public void draw(final Graphics2D g2, final Rectangle2D area) {
				final AffineTransform atPrev = g2.getTransform();
				try {
					g2.translate( area.getX(), area.getY() );
					c.paint( g2 );
				} finally {
					g2.setTransform( atPrev );
				}
			}
		};
		
		return new XYDrawableAnnotation(x, y, c.getWidth(), c.getHeight(), d);
	}
	
	//--------------------------------------------------
    
	static public JEditorPane createHTMLEditorPane(final String text, int width) {		
		final JEditorPane pane = new JEditorPane();
		pane.setEditable(false);
		pane.setContentType("text/html");
		pane.setText(text);
		pane.setSize(new Dimension(width,100));
		pane.validate();
		return pane;
	}

	//--------------------------------------------------
    
	/**
     * Creates a dataset, consisting of two series of monthly data.
     *
     * @return The dataset.
     */
    private static XYDataset createDataset() {

        TimeSeries s1 = new TimeSeries("L&G European Index Trust", Month.class);
        s1.add(new Month(2, 2001), 181.8);
        s1.add(new Month(3, 2001), 167.3);
        s1.add(new Month(4, 2001), 153.8);
        s1.add(new Month(5, 2001), 167.6);
        s1.add(new Month(6, 2001), 158.8);
        s1.add(new Month(7, 2001), 148.3);
        s1.add(new Month(8, 2001), 153.9);
        s1.add(new Month(9, 2001), 142.7);
        s1.add(new Month(10, 2001), 123.2);
        s1.add(new Month(11, 2001), 131.8);
        s1.add(new Month(12, 2001), 139.6);
        s1.add(new Month(1, 2002), 142.9);
        s1.add(new Month(2, 2002), 138.7);
        s1.add(new Month(3, 2002), 137.3);
        s1.add(new Month(4, 2002), 143.9);
        s1.add(new Month(5, 2002), 139.8);
        s1.add(new Month(6, 2002), 137.0);
        s1.add(new Month(7, 2002), 132.8);

        TimeSeries s2 = new TimeSeries("L&G UK Index Trust", Month.class);
        s2.add(new Month(2, 2001), 129.6);
        s2.add(new Month(3, 2001), 123.2);
        s2.add(new Month(4, 2001), 117.2);
        s2.add(new Month(5, 2001), 124.1);
        s2.add(new Month(6, 2001), 122.6);
        s2.add(new Month(7, 2001), 119.2);
        s2.add(new Month(8, 2001), 116.5);
        s2.add(new Month(9, 2001), 112.7);
        s2.add(new Month(10, 2001), 101.5);
        s2.add(new Month(11, 2001), 106.1);
        s2.add(new Month(12, 2001), 110.3);
        s2.add(new Month(1, 2002), 111.7);
        s2.add(new Month(2, 2002), 111.0);
        s2.add(new Month(3, 2002), 109.6);
        s2.add(new Month(4, 2002), 113.2);
        s2.add(new Month(5, 2002), 111.6);
        s2.add(new Month(6, 2002), 108.8);
        s2.add(new Month(7, 2002), 101.6);

        // ******************************************************************
        //  More than 150 demo applications are included with the JFreeChart
        //  Developer Guide...for more information, see:
        //
        //  >   http://www.object-refinery.com/jfreechart/guide.html
        //
        // ******************************************************************
        
        TimeSeriesCollection dataset = new TimeSeriesCollection();
        dataset.addSeries(s1);
        dataset.addSeries(s2);
        
        return dataset;

    }
    
    /**
     * Saves a chart to a PDF file. 
     *  
     * @param file  the file. 
     * @param chart  the chart.
     * @param width  the chart width.
     * @param height  the chart height.
     * @param mapper  the font mapper.
     * 
     * @throws IOException if there is an I/O problem.
     */
    public static void saveChartAsPDF(File file,
                                      JFreeChart chart,
                                      int width,
                                      int height,
                                      FontMapper mapper) throws IOException {
        
        OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
        writeChartAsPDF(out, chart, width, height, mapper);
        out.close();
            
    } 
    
    /**
     * Writes a chart to an output stream in PDF format. 
     *  
     * @param out  the output stream. 
     * @param chart  the chart. 
     * @param width  the chart width.
     * @param height  the chart height.
     * @param mapper  the font mapper.
     * 
     * @throws IOException if there is an I/O problem.
     */
    public static void writeChartAsPDF(OutputStream out,
                                       JFreeChart chart,
                                       int width,
                                       int height,
                                       FontMapper mapper) throws IOException {

        Rectangle pagesize = new Rectangle(width, height);
        Document document = new Document(pagesize, 50, 50, 50, 50);
        try {
            PdfWriter writer = PdfWriter.getInstance(document, out);
            document.addAuthor("JFreeChart");
            document.addSubject("Demonstration");
            document.open();
            PdfContentByte cb = writer.getDirectContent();
            PdfTemplate tp = cb.createTemplate(width, height);
            Graphics2D g2 = tp.createGraphics(width, height, mapper);
            Rectangle2D r2D = new Rectangle2D.Double(0, 0, width, height);
            chart.draw(g2, r2D);
            
            // Uncomment this to draw the JEditorPane directly into the PDF
//			final AffineTransform atPrev = g2.getTransform();
//			try {
//				g2.translate( 225, 50 );
//				createHTMLEditorPane( defaultHTML, 150 ).paint(g2);
//			} finally {
//				g2.setTransform( atPrev );
//			}
            
            g2.dispose();
            cb.addTemplate(tp, 0, 0);
        } 
        catch (DocumentException de) {
            System.err.println(de.getMessage());
        }
        document.close();
    }    

    /**
     * Creates a panel for the demo (used by SuperDemo.java).
     * 
     * @return A panel.
     */
    public static JPanel createDemoPanel() {
        JFreeChart chart = createChart(createDataset());
        return new ChartPanel(chart);
    }
    
    /**
     * Starting point for the demonstration application.
     *
     * @param args  ignored.
     * @throws IOException 
     */
    public static void main(String[] args) throws IOException {

        TimeSeriesChartDemoX demo = new TimeSeriesChartDemoX(
                "Time Series Chart Demo X");
        demo.pack();
        RefineryUtilities.centerFrameOnScreen(demo);
        demo.setVisible(true);

        // write the chart to a PDF file... 
        File fileName = new File(System.getProperty("user.home") 
                                 + "/TimeSeriesChartDemoX.pdf"); 
        saveChartAsPDF(fileName, demo.chart, 400, 300, new DefaultFontMapper()); 
    }

}

david.gilbert
JFreeChart Project Leader
Posts: 11734
Joined: Fri Mar 14, 2003 10:29 am
antibot: No, of course not.
Contact:

Re: XYDrawableAnnotation with JEditorPane to render HTML

Post by david.gilbert » Wed Mar 26, 2008 10:12 pm

mickish wrote:The fact that I can write a JEditorPane directly into the PDF, but not via the annotation system, suggests that there is more that I need to take into account when dealing with the annotation system.
There shouldn't be anything else that you need to take account of. I'm able to reproduce the bug, but I don't know the cause of it. I'll try a few things out...
David Gilbert
JFreeChart Project Leader

:idea: Read my blog
:idea: Support JFree via the Github sponsorship program

david.gilbert
JFreeChart Project Leader
Posts: 11734
Joined: Fri Mar 14, 2003 10:29 am
antibot: No, of course not.
Contact:

Post by david.gilbert » Thu Mar 27, 2008 1:18 pm

It seems that iText's PdfGraphics2D class doesn't like our alpha composite setting. Try inserting the following line right before your commented out code that draws the editor pane to the PDF:

Code: Select all

   g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 
                    1.0f));
If you do this, you'll see that the HTML output is also invisible (transparent?) in the PDF, just like with the annotation. I suspect this is an iText bug, but I can't yet figure out exactly how their implementation of PdfGraphics2D works, so I can't be 100% sure.
David Gilbert
JFreeChart Project Leader

:idea: Read my blog
:idea: Support JFree via the Github sponsorship program

mickish
Posts: 29
Joined: Tue Jan 08, 2008 11:15 pm
Location: Venice, Florida

Post by mickish » Thu Mar 27, 2008 4:49 pm

Thanks, that is very helpful!

As a workaround, I now set AlphaComposite.SRC in my Drawable's draw() method, which means "overwrite the destination with the source, regardless of alpha":

Code: Select all

g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC, 1.0f));

Locked