Background thread for drawing a chart

A discussion forum for JFreeChart (a 2D chart library for the Java platform).
Locked
caa
Posts: 21
Joined: Mon Feb 14, 2005 11:28 pm

Background thread for drawing a chart

Post by caa » Sun Jul 02, 2006 2:59 am

I wrote a little proxy around the JFreeChart.draw method. It kicks off the drawing onto a seperate thread. It seems to work pretty good, I'd like to hear what other people out there think of it. I had originally done this by making my own version of ChartPanel, but this is much better.

ThreadChart.java

Code: Select all

/*
 * Created on Jul 1, 2006
 *
 * Spawn a thread to do chart drawing.
 */

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;

import org.jfree.chart.ChartRenderingInfo;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.event.ChartChangeEvent;
import org.jfree.chart.event.ChartChangeEventType;
import org.jfree.chart.event.ChartChangeListener;
import org.jfree.chart.event.ChartProgressEvent;
import org.jfree.chart.event.ChartProgressListener;
import org.jfree.chart.plot.Plot;

import edu.emory.mathcs.backport.java.util.concurrent.Callable;
import edu.emory.mathcs.backport.java.util.concurrent.ExecutorService;
import edu.emory.mathcs.backport.java.util.concurrent.Executors;
import edu.emory.mathcs.backport.java.util.concurrent.Future;

/**
 * @author Charles Anderson
 * 
 * This class creates a proxy around the main JFreeChart draw method. It creates
 * a new BufferedImage that is passed to the superclass draw method in a new
 * thread. When it receives a ChartProgress.DRAWING_FINISHED event it fires a
 * ChartChanged back to the ChartPanel, which causes it to redraw. When we get
 * the next call to draw we simply fill graphics with the imgBuffer that was
 * drawn by JFreeChart.
 */
public class ThreadChart extends JFreeChart implements ChartProgressListener, ChartChangeListener {
    private BufferedImage imgBuffer = null;
    private ExecutorService exec = Executors.newSingleThreadExecutor();
    private Future chartDrawing;
    private Font font = new Font("Dialog", Font.PLAIN, 14);
    private int width;
    private int height;

    /**
     * @param title
     * @param titleFont
     * @param plot
     * @param createLegend
     */
    public ThreadChart(String title, Font titleFont, Plot plot, boolean createLegend) {
        super(title, titleFont, plot, createLegend);
        addListeners();
    }

    /**
     * @param title
     * @param plot
     */
    public ThreadChart(String title, Plot plot) {
        super(title, plot);
        addListeners();
    }

    /**
     * @param plot
     */
    public ThreadChart(Plot plot) {
        super(plot);
        addListeners();
    }

    /**
     * 
     */
    private void addListeners() {
        super.addProgressListener(this);
        super.addChangeListener(this);
    }

    public void superDraw(Graphics2D g2, final Rectangle2D chartArea, final Point2D anchor, final ChartRenderingInfo info) {
        super.draw(g2, chartArea, anchor, info);
    }

    public void draw(Graphics2D g2, final Rectangle2D chartArea, final Point2D anchor, final ChartRenderingInfo info) {
        Rectangle d = chartArea.getBounds();
        int w = d.width;
        int h = d.height;
        if (imgBuffer != null && w == this.width && h == this.height) {
            g2.drawImage(imgBuffer, 0, 0, null);
            return;
        }
        else {
            if (chartDrawing != null) {
                chartDrawing.cancel(true);
            }
            this.width = d.width;
            this.height = d.height;
            imgBuffer = new BufferedImage(d.width, d.height, BufferedImage.TYPE_INT_RGB);
            chartDrawing = exec.submit(new DrawTask((Graphics2D)imgBuffer.getGraphics(), chartArea, anchor, info));
        }
        if (getBackgroundPaint() != null) {
            g2.setPaint(getBackgroundPaint());
            g2.fill(chartArea);
        }

        drawCenteredString(g2, chartArea, "Drawing Chart");
    }

    /**
     * @author Charles Anderson
     * 
     */
    private class DrawTask implements Callable {
        private Graphics2D g2;
        private Rectangle2D chartArea;
        private Point2D anchor;
        private ChartRenderingInfo info;

        public DrawTask(Graphics2D g2, Rectangle2D chartArea, Point2D anchor, ChartRenderingInfo info) {
            this.g2 = g2;
            this.chartArea = chartArea;
            this.anchor = anchor;
            this.info = info;
        }

        public Object call() throws Exception {
            superDraw(g2, chartArea, anchor, info);
            if (!Thread.interrupted())
                fireChange();
            return null;
        }
    }

    /**
     * @param g2
     * @param chartArea
     * @param text
     */
    private void drawCenteredString(Graphics2D g2, Rectangle2D chartArea, String text) {
        Rectangle d = chartArea.getBounds();
        int w = d.width;
        int h = d.height;
        g2.setColor(Color.BLACK);
        FontRenderContext frc = g2.getFontRenderContext();
        TextLayout tl = new TextLayout(text, font, frc);
        float tx = w / 2 - tl.getAdvance() / 2;
        float ty = h / 2 - tl.getAscent() / 2;
        tl.draw(g2, tx, ty);
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.jfree.chart.event.ChartProgressListener#chartProgress(org.jfree.chart.event.ChartProgressEvent)
     */
    public void chartProgress(ChartProgressEvent event) {
        if (event.getType() == ChartProgressEvent.DRAWING_FINISHED && chartDrawing != null && chartDrawing.isDone()
                && !chartDrawing.isCancelled()) {
            fireChange();
        }
    }

    private void fireChange() {
        chartDrawing = null;
        if (!EventQueue.isDispatchThread()) {
            EventQueue.invokeLater(new Runnable() {
                public void run() {
                    fireChartChanged();
                }
            });
        }
        else {
            fireChartChanged();
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.jfree.chart.event.ChartChangeListener#chartChanged(org.jfree.chart.event.ChartChangeEvent)
     */
    public void chartChanged(ChartChangeEvent event) {
        if (event.getType() == ChartChangeEventType.DATASET_UPDATED || event.getType() == ChartChangeEventType.NEW_DATASET
                || event.getSource() != this) {
            imgBuffer = null;
        }
    }
}
Here is a little test program, that I hacked up from something that was posted here a while ago....So if it looks kind of familiar to someone, that's why.

TestThreadChart.java

Code: Select all

/*
 * Created on May 16, 2006
 *
 * TODO To change the template for this generated file go to
 * Window - Preferences - Java - Code Style - Code Templates
 */

import javax.swing.JPanel;

import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.labels.StandardXYToolTipGenerator;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.ui.ApplicationFrame;
import org.jfree.ui.RefineryUtilities;

/**
 * A simple demo showing a dataset created using the {@link XYSeriesCollection}
 * class.
 */
public class TestThreadChart extends ApplicationFrame {

    public TestThreadChart(String title) {
        super(title);
        XYDataset dataset = createDataset();
        JFreeChart chart = createChart(dataset);
        ChartPanel chartPanel = new ChartPanel(chart, true);
        chartPanel.setPreferredSize(new java.awt.Dimension(500, 270));
        setContentPane(chartPanel);
    }

    private static JFreeChart createChart(XYDataset dataset) {
        NumberAxis xAxis = new NumberAxis("X");
        xAxis.setAutoRangeIncludesZero(false);
        NumberAxis yAxis = new NumberAxis("Y");
        XYItemRenderer renderer = new XYLineAndShapeRenderer(true, false);
        XYPlot plot = new XYPlot(dataset, xAxis, yAxis, renderer);
        plot.setOrientation(PlotOrientation.VERTICAL);
        renderer.setBaseToolTipGenerator(new StandardXYToolTipGenerator());

        JFreeChart chart = new ThreadChart("XY Series Demo", JFreeChart.DEFAULT_TITLE_FONT, plot, true);

        return chart;
    }

    private static XYDataset createDataset() {
        XYSeries series = new XYSeries("Random Data");
        double t = 0.0;
        double x = 0.0;

        for (int i = 0; i < 100000; i++) {
            t += Math.random();
            double r = Math.random();
            if (r < 0.33) {
                x += Math.random();
            }
            else if (r < 0.66) {
                x -= Math.random();
            }
            series.add(t, x);
        }

        return new XYSeriesCollection(series);
    }

    public static JPanel createDemoPanel() {
        JFreeChart chart = createChart(createDataset());
        return new ChartPanel(chart);
    }

    public static void main(String[] args) {
        TestThreadChart demo = new TestThreadChart("XY Series Demo");
        demo.pack();
        RefineryUtilities.centerFrameOnScreen(demo);
        demo.setVisible(true);
    }
}
The 100000 item series takes about 5 seconds for JFreeChart to draw on my box here.
While it's doing that it puts a message saying "Drawing Chart" in the middle of the graphics2d that was passed in from the ChartPanel paintComponent method, which is returned immediately.
If you click on the chart while it's drawing you can actually see the partially drawn plot. I'm not sure if this is a good thing or not. There may need to be a check added to not copy the partially drawn image.
Swing remains responsive, and you can right click on the panel and the popup comes right up.
It does produce a little bit of an extra lag on small charts, so I don't think it would be something you want to use all the time.

I'm using the util-concurrent backport, because I'm relagated to java 1.4.
If you're using java 5 you can just change the import.
If you're stuck on 1.4 like me, the port is at http://www.mathcs.emory.edu/dcl/util/ba ... concurrent

I added the call to fireChange from DrawTask because I was getting the chartProgress event before the Thread was marked as done.
So really at this point the ChartProgress listener isn't really neccesary.
Last edited by caa on Mon Jul 03, 2006 3:20 am, edited 3 times in total.
Charles Anderson

caa
Posts: 21
Joined: Mon Feb 14, 2005 11:28 pm

Post by caa » Sun Jul 02, 2006 8:13 pm

I just edited, my code, I changed the drawing task so that it is cancelled if another draw comes in before the previous one is done.

Also if anyone knows if there is a way to call super.draw from the inner class I'd like to know.

I tested the cancelling by resizing my window several times while drawing, causing new draw requests to be generated. It seemed to work good, the SingleThreadExecutor gaurantees that we won't reenter JFreeChart.draw while it's working on a previous draw request.
Charles Anderson

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 » Mon Jul 03, 2006 4:17 pm

This looks interesting...what's the chances of the util-concurrent backport working on JDK1.3, which is the minimum requirement for JFreeChart 1.0.x?
David Gilbert
JFreeChart Project Leader

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

caa
Posts: 21
Joined: Mon Feb 14, 2005 11:28 pm

Post by caa » Mon Jul 03, 2006 9:56 pm

Not too good, it's got a bunch of asserts in it which were added in 1.4
I could probably change it to use Doug Lea's concurrent package which the java.util.concurrent was based on, but there were changes, so it's not drop-in replaceable.
Charles Anderson

Jii
Posts: 128
Joined: Thu Apr 21, 2005 11:17 am

Post by Jii » Wed Jul 05, 2006 7:04 am

Looks like you can't use this if you plan on doing any custom drawing in chartpanel :/

caa
Posts: 21
Joined: Mon Feb 14, 2005 11:28 pm

Post by caa » Thu Jul 06, 2006 2:19 am

If you customize ChartPanel, you can add this functionality into the paintComponent method. That's how I originally implemented it, but I decided that I didn't want to worry about merging changes to ChartPanel into my code, instead I can just drop in new jfreechart jars.
Charles Anderson

Jii
Posts: 128
Joined: Thu Apr 21, 2005 11:17 am

Post by Jii » Thu Jul 06, 2006 11:20 am

What i meant was custom painting in an overridden paint(Graphics). It causes a bunch of flickering etc :/

caa
Posts: 21
Joined: Mon Feb 14, 2005 11:28 pm

Post by caa » Thu Jul 06, 2006 1:11 pm

If you're overiding paint, this shouldn't make a difference, but why would you want to overide paint? Do you want the flickering?
I've never tried to overide paint, and I don't think I want to, worry about all the clip rectangles.
Charles Anderson

cringe
Posts: 54
Joined: Wed May 10, 2006 10:39 am
Location: Germany
Contact:

Post by cringe » Fri Jul 07, 2006 6:27 am

Yes, the clipping is a tricky part. But you can override the paint methods to implement your own crosshair for example. But you have to put a lot of time into the clipping and drawing - and I don't think it's necessary to change such a basic feature. If it comes to zooming mouse behavior, that's a different part: I don't think that the zoom behavior is very intuitive and I override that in my application.

But there is the same problem now: Switching JFreeChart to a newer version is almost impossible, because the application started with a pre-1.0 version. And now there are too many old API calls to just exchange the JARs... so take another thought on overriding JFreeChart functions in your own components. :wink:

Jii
Posts: 128
Joined: Thu Apr 21, 2005 11:17 am

Post by Jii » Fri Jul 07, 2006 8:54 am

caa wrote:If you're overiding paint, this shouldn't make a difference, but why would you want to overide paint? Do you want the flickering?
I've never tried to overide paint, and I don't think I want to, worry about all the clip rectangles.
Custom crosshair, custom zoom rectangle, that sort of thing.

cringe
Posts: 54
Joined: Wed May 10, 2006 10:39 am
Location: Germany
Contact:

Post by cringe » Fri Jul 07, 2006 9:25 am

After implementing a vertical line that moves forward each second, I had several clipping errors with overlapping JInternalFrames. I placed this line into my drawing method and the clipping issue was gone:

Code: Select all

mr_obj_g2.clip(new Rectangle(0, 0, 0, 0));
I don't know if it's the best method, but it works. :)

caa
Posts: 21
Joined: Mon Feb 14, 2005 11:28 pm

Post by caa » Fri Jul 07, 2006 2:15 pm

OK, I didn't have a problem with how zoom works. But still even if you have your own ChartPanel, I extended JFreeChart, you should still be able to pass ThreadChart to your modified ChartPanel, and as long as you still call JFreeChart.draw(), someplace in paintComponent() (and I don't see how you can get a chart without it.) you should be able to use this.

Your custom drawing will get done on top of what is returned, but you might need an indicator not to do your custom drawing until the actual chart is returned, not just my "Drawing Chart" blank that is returned when the thread is kicked off.
Charles Anderson

azr
Posts: 13
Joined: Sun Apr 29, 2007 6:05 pm

Post by azr » Sun Apr 29, 2007 6:18 pm

Hi,
I'm trying to subclass the ChartPanel class to delegate the rendering to a background thread.

But i have a problem about the swap of my buffers. I want to do this when the image have been displayed and to knwo that i tried to use the ImageUpdate method, provided by the ImageObserver interface.

But this never is never called...
If someone can explain me why... :D

Code: Select all

/*
 */

package ChartBeanPackage;

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.event.ChartProgressEvent;

/**
 */
public class PanelOptimized extends ChartPanel implements ImageObserver
{
    
    /** For serialization. */
    private static final long serialVersionUID = 6046366197214274674L;
    
    private ScheduledExecutorService scheduledExecutorService;
    
    private BufferedImage backImage;
    private BufferedImage frontImage;
    
    private Graphics graphicsFromBackImage;
    
    private int backImageWidth;
    private int backImageHeight;
    
    private boolean readyToDraw;
    
    private PanelRenderThread panelRenderThread;
    
    /** Creates a new instance of PanelOptimized */
    public PanelOptimized(JFreeChart chart)
    {
        super(chart,320,200,300,200,800,600,false,false,false,false,false,false);
        this.scheduledExecutorService = new ScheduledThreadPoolExecutor(1);
        this.panelRenderThread = new PanelRenderThread(this);
        this.readyToDraw = true;
        this.backImageHeight = 0;
        this.backImageWidth = 0;
    }
    
    public PanelOptimized(JFreeChart chart,boolean extraMenu)
    {
        super(chart,320,200,300,200,800,600,false,extraMenu,extraMenu,extraMenu,extraMenu,extraMenu);
        this.scheduledExecutorService = new ScheduledThreadPoolExecutor(1);
        this.panelRenderThread = new PanelRenderThread(this);
        this.readyToDraw = true;
        this.backImageHeight = 0;
        this.backImageWidth = 0;
    }
    
    public void paintComponent(Graphics g)
    {
        if (backImage == null)
        {
            super.paintComponent(g);
        }
        //we check the size of the thread pool queue before scheduling a new task
        //if there is already a scheduled task we don t schedule a new one
        if (((ScheduledThreadPoolExecutor)this.scheduledExecutorService).getQueue().size()<1)
        {
            this.scheduledExecutorService.schedule(this.panelRenderThread,0,TimeUnit.MILLISECONDS);
        }        
        this.display(g);
        
    }
    
    public void  render()
    {
        
        // first determine the size of the chart rendering area...
        Dimension size = this.getSize();
        Insets insets = this.getInsets();
        Rectangle2D available = new Rectangle2D.Double(insets.left, insets.top,
                size.getWidth() - insets.left - insets.right,
                size.getHeight() - insets.top - insets.bottom);
        
        //creation of the backImage
        if ((this.backImage == null)
        || (this.backImageWidth != available.getWidth())
        || (this.backImageHeight != available.getHeight())
        )
        {
            this.backImageWidth = (int)available.getWidth();
            this.backImageHeight = (int)available.getHeight();
            this.backImage = new BufferedImage(this.backImageWidth, this.backImageHeight,BufferedImage.TYPE_INT_RGB);
        }
        
        //create the graphics from the backImage
        this.graphicsFromBackImage = this.backImage.createGraphics();
        synchronized(this)
        {
            //if the drawing is not already in progress
            if (this.readyToDraw == true)
            {
                //draw the chart in the backImage
                super.paintComponent(this.graphicsFromBackImage);
                
                
            }
        }
        //release the graphics
        this.graphicsFromBackImage.dispose();
//        System.out.println("render finished");
    }
    
    
    public void display(Graphics g)
    {
        
        if (this.frontImage != null)
        {
            g.drawImage(this.frontImage,0,0,null);
            
        }
    }
    
    private void swapBuffers()
    {
        BufferedImage tempImage;
        tempImage = this.frontImage;
        this.frontImage = this.backImage;
        this.backImage= tempImage;
    }
    
    /**
     * Receives notification of a chart progress event.
     *
     * @param event  the event.
     */
    public void chartProgress(ChartProgressEvent event)
    {
        if (event.getType()==event.DRAWING_FINISHED)
        {
            synchronized(this)
            {
                this.swapBuffers();
                this.readyToDraw = true;
            }
        }
        else
        {
            if (event.getType()==event.DRAWING_STARTED)
            {
                synchronized(this)
                {
                    this.readyToDraw = false;
                }
            }
        }
    }    
}


Last edited by azr on Thu May 03, 2007 9:05 pm, edited 1 time in total.

azr
Posts: 13
Joined: Sun Apr 29, 2007 6:05 pm

Post by azr » Thu May 03, 2007 9:04 pm

Well,
i'm still having a problem in my draw ...
The method imageUpdate seems to not be called because the imageDraw is too fast :D

But it seems that i begin a new draw when the old one is not finished... Do you have encountered the same problem?

An illustration of the problem :
img228.imageshack.us/img228/1746/bugio4.jpg

azr
Posts: 13
Joined: Sun Apr 29, 2007 6:05 pm

Post by azr » Tue May 15, 2007 10:05 am

In fact, that was not the problem.
I think that is due to the fact that the drawing is made in 2 steps and that the model is not thread safe.
I think that points are added during the drawing of the line, and so when the shapes are drawn, there is an index problem.
But i can't synchronized it... :?

Locked