To Those Who Use JFreeChart for Dynamic Plotting/Large Sets

A discussion forum for JFreeChart (a 2D chart library for the Java platform).
Locked
evan
Posts: 14
Joined: Thu Feb 08, 2007 6:26 pm

Re: Jfreechart - 1.0.3

Post by evan » Thu Feb 08, 2007 10:34 pm

adrsingle wrote:does the last version of Jfreechart has these classes
I'd very much like to know as well. Thanks.

skunk
Posts: 1087
Joined: Thu Jun 02, 2005 10:14 pm
Location: Brisbane, Australia

Post by skunk » Thu Feb 08, 2007 10:37 pm

Why don't you look at the provided source code to find out?

gwk
Posts: 2
Joined: Fri Feb 09, 2007 1:19 am
Location: Pittsburgh, PA

Post by gwk » Fri Feb 09, 2007 1:29 am

I have added another mirror for the code, just in case.

You can find it here:
kronenberger.dreamhosters.com/Code/JFreeChart/JFreeChartFAST.zip

SamiLakka
Posts: 8
Joined: Tue May 02, 2006 2:22 pm

FastXYPlot

Post by SamiLakka » Mon Mar 19, 2007 8:45 am

I wrote a new version from the FastXYPlot renderFast method. My problem was that I have datasets that have lots of one pixel wide peaks and the previous version seemed to not draw them. This version isn't perfect but it seems to work relatively fast.

Code: Select all

/**
	 * Performs fast rendering. Skips overlapping pixels
	 */
	private void renderFast(ValueAxis xAxis, ValueAxis yAxis, XYDataset dataset,
			int series, XYItemRenderer renderer, XYItemRendererState state,
			int pass, Graphics2D g2, Rectangle2D dataArea,
			PlotRenderingInfo info, CrosshairState crosshairState) {

		// Contains the x-values for the current series
		int count = dataset.getItemCount(series);
		double[] xValues = new double[count];		
				
		for (int item = 0; item < count; item++) 
			xValues[item] = dataset.getXValue(series, item);
		
		int firstItem = findItemIndex(xValues, xAxis.getLowerBound(), count);
		int lastItem = findItemIndex(xValues, xAxis.getUpperBound(), count);

		RectangleEdge domainEdge = getDomainAxisEdge();
		RectangleEdge rangeEdge = getDomainAxisEdge();
		
		double xAxisUpperBound = xAxis.getUpperBound();
		double yAxisUpperBound = yAxis.getUpperBound();
		int maxX = (int) Math.ceil(xAxis.valueToJava2D(xAxisUpperBound,dataArea,domainEdge)) + 1;
		int maxY = (int) Math.ceil(yAxis.valueToJava2D(yAxisUpperBound,dataArea,rangeEdge))  + 1;
		
		boolean drawnArray[][] = new boolean[maxX][maxY];
				
		for (int item = firstItem; item <= lastItem; item++) {

			double xValue = dataset.getXValue(series, item);
			double yValue = dataset.getYValue(series,item);
			double xCoordinate = xAxis.valueToJava2D(xValue, dataArea, domainEdge);
			double yCoordinate = yAxis.valueToJava2D(yValue,dataArea,rangeEdge);			
			int roundedX = (int) Math.round(xCoordinate);
			int roundedY = (int) Math.round(yCoordinate);
			
			// This happens if we are doing double zooming, i.e. zooming
			// before the previous zoom result has been drawn.			
			if (roundedX >= 0 && roundedY >= 0 && roundedX < maxX && roundedY < maxY) {
				
				if (drawnArray[roundedX][roundedY] == true) 
					continue;
			
				drawnArray[roundedX][roundedY] = true;
			}
			
			renderer.drawItem(g2, state, dataArea, info, this, xAxis,
						yAxis, dataset, series, item, crosshairState, pass);
								
		}
	}

private int findItemIndex(double[] values, double value, int maxCount) {		
		int itemIndex = Arrays.binarySearch(values, value);
		
		if (itemIndex < 0) 
			itemIndex = -itemIndex - 1;
		
		itemIndex = Math.max(0, Math.min(itemIndex, maxCount - 1));
		return itemIndex;
	}


EDIT: Made a little change so that the last item is also rendered.

bryce
Posts: 2
Joined: Fri Jan 12, 2007 11:12 pm

Post by bryce » Mon Mar 19, 2007 10:27 pm

Will there be any plans to include the "FAST classes" into the release branch of JFreeChart before the release of version 2?

nonlinear5
Posts: 6
Joined: Wed Dec 27, 2006 1:03 am

Post by nonlinear5 » Sat Jul 21, 2007 6:14 am

Here is another (much simplified) version. Renders my 250,000 data points in under a second. In fact, the speed is nearly constant, no matter what the size of the dataset, or how much data is displayed when zoomed in or out. That's simply because it samples the data points when their size exceeds that of the data area. As before, this will work with any renderer.

Code: Select all

package com.jsystemtrader.chart;

import org.jfree.chart.axis.*;
import org.jfree.chart.plot.*;
import org.jfree.chart.renderer.xy.*;
import org.jfree.data.general.*;
import org.jfree.data.xy.*;

import java.awt.*;
import java.awt.geom.*;
import java.util.*;

class FastXYPlot extends XYPlot {

    public FastXYPlot(XYDataset dataset, ValueAxis domainAxis, ValueAxis rangeAxis, XYItemRenderer renderer) {
        super(dataset, domainAxis, rangeAxis, renderer);
    }

    private void renderFast(ValueAxis xAxis, ValueAxis yAxis, XYDataset dataset, int series, XYItemRenderer renderer,
                            XYItemRendererState state, int pass, Graphics2D g2, Rectangle2D dataArea,
                            PlotRenderingInfo info, CrosshairState crosshairState) {

        int count = dataset.getItemCount(series);
        double[] xValues = new double[count];

        for (int item = 0; item < count; item++) {
            xValues[item] = dataset.getXValue(series, item);
        }

        int firstItem = findItemIndex(xValues, xAxis.getLowerBound(), count);
        int lastItem = findItemIndex(xValues, xAxis.getUpperBound(), count);

        double width = Math.max(dataArea.getWidth(), 1);
        int itemsPerPixel = (int) Math.max(1, (lastItem - firstItem) / width);

        for (int item = firstItem; item < lastItem; item += itemsPerPixel) {
            renderer.drawItem(g2, state, dataArea, info, this, xAxis, yAxis, dataset, series, item, crosshairState, pass);
        }
    }


    public boolean render(Graphics2D g2, Rectangle2D dataArea, int index, PlotRenderingInfo info,
                          CrosshairState crosshairState) {
        boolean foundData = false;
        XYDataset dataset = getDataset(index);
        if (!DatasetUtilities.isEmptyOrNull(dataset)) {
            foundData = true;
            ValueAxis xAxis = getDomainAxisForDataset(index);
            ValueAxis yAxis = getRangeAxisForDataset(index);
            XYItemRenderer renderer = getRenderer(index);
            if (renderer == null) {
                renderer = getRenderer();
            }

            XYItemRendererState state = renderer.initialise(g2, dataArea, this, dataset, info);
            int passCount = renderer.getPassCount();

            SeriesRenderingOrder seriesOrder = getSeriesRenderingOrder();

            if (seriesOrder == SeriesRenderingOrder.REVERSE) {
                //render series in reverse order
                for (int pass = 0; pass < passCount; pass++) {
                    int seriesCount = dataset.getSeriesCount();
                    for (int series = seriesCount - 1; series >= 0; series--) {
                        renderFast(xAxis, yAxis, dataset, series, renderer, state, pass, g2, dataArea, info,
                                crosshairState);
                    }
                }
            } else {
                //render series in forward order
                for (int pass = 0; pass < passCount; pass++) {
                    int seriesCount = dataset.getSeriesCount();
                    for (int series = 0; series < seriesCount; series++) {
                        renderFast(xAxis, yAxis, dataset, series, renderer, state, pass, g2, dataArea, info,
                                crosshairState);
                    }
                }
            }
        }
        return foundData;
    }

    private int findItemIndex(double[] values, double value, int maxCount) {
        int itemIndex = Arrays.binarySearch(values, value);

        if (itemIndex < 0)
            itemIndex = -itemIndex - 1;

        itemIndex = Math.max(0, Math.min(itemIndex, maxCount));
        return itemIndex;
    }

}


nonlinear5
Posts: 6
Joined: Wed Dec 27, 2006 1:03 am

Post by nonlinear5 » Sat Jul 21, 2007 7:13 am

I just noticed that version 1.0.6 has been released with the performance improvments, so I downloaded and tried it. Nice job with the state.getProcessVisibleItemsOnly() in XYPlot, DG! There is a very noticeble speed up when zoomed-in. However, it still takes an eternity when zoomed out: about 50 seconds to render my data set. As I mentioned above, it takes less than a second with the FastXYPlot which I posted. Granted, it's a hack, but I believe it could be turned into a real thing which could be a permanent part of JFreeChart.

The key idea is very simple: since there are only so many pixels in the data area, you can't display a large data set without the pixel overlaping. As it is in the latest release, XYPlot still calls renderer.drawItem() for every single visible item, even if the items overlap (that is, rendered in exatly same pixels). What I suggest can be done is that only the non-overlapping items can be calculated in advance, and then passed to the renderer. That way, the resulting plot would look exactly the same as if rendered with XYPlot in release 1.0.6, but it would take no more than a second or so to render, no matter what the size of the dataset and no matter what the zoom is.

nonlinear5
Posts: 6
Joined: Wed Dec 27, 2006 1:03 am

Post by nonlinear5 » Fri Aug 03, 2007 5:01 am

Here is the latest and greatest version that I have. It takes advantage of JFreeChart 1.0.6 improvements and also borrows from SamiLakka's ideas. It renders much more precisely, not distinguishable from a standard XYPlot, but much much faster. Hope this makes it into one of the next releases.

Code: Select all

package com.jsystemtrader.chart;

import org.jfree.chart.axis.*;
import org.jfree.chart.plot.*;
import org.jfree.chart.renderer.*;
import org.jfree.chart.renderer.xy.*;
import org.jfree.data.general.*;
import org.jfree.data.xy.*;
import org.jfree.ui.*;

import java.awt.*;
import java.awt.geom.*;
import java.util.*;

/**
 * Performs fast rendering of large datasets in nearly constant time.
 */
class FastXYPlot extends XYPlot {

    private final HashSet<Integer> renderedPixels = new HashSet<Integer>();

    public FastXYPlot(XYDataset dataset, ValueAxis domainAxis, ValueAxis rangeAxis, XYItemRenderer renderer) {
        super(dataset, domainAxis, rangeAxis, renderer);
    }

    /**
     * Determines if the item is to be rendered in the area of the plot where one of the previous
     * items has already been rendered.
     */
    private boolean hasRendered(XYDataset dataset, ValueAxis xAxis, ValueAxis yAxis, RectangleEdge domainEdge,
                                RectangleEdge rangeEdge, Rectangle2D dataArea, int series, int item) {
        boolean hasRendered = true;

        int width = (int) dataArea.getWidth();

        double xValue = dataset.getXValue(series, item);
        double yValue = dataset.getYValue(series, item);
        int x = (int) xAxis.valueToJava2D(xValue, dataArea, domainEdge);
        int y = (int) yAxis.valueToJava2D(yValue, dataArea, rangeEdge);

        int itemKey = x + width * y;
        if (!renderedPixels.contains(itemKey)) {
            renderedPixels.add(itemKey);
            hasRendered = false;
        }

        return hasRendered;
    }


    @Override
    public boolean render(Graphics2D g2,
                          Rectangle2D dataArea,
                          int index,
                          PlotRenderingInfo info,
                          CrosshairState crosshairState) {

        boolean foundData = false;
        XYDataset dataset = getDataset(index);
        if (!DatasetUtilities.isEmptyOrNull(dataset)) {
            foundData = true;
            ValueAxis xAxis = getDomainAxisForDataset(index);
            ValueAxis yAxis = getRangeAxisForDataset(index);
            XYItemRenderer renderer = getRenderer(index);
            if (renderer == null) {
                renderer = getRenderer();
                if (renderer == null) { // no default renderer available
                    return foundData;
                }
            }

            XYItemRendererState state = renderer.initialise(g2, dataArea, this,
                    dataset, info);
            int passCount = renderer.getPassCount();
            renderedPixels.clear();
            RectangleEdge domainEdge = getDomainAxisEdge();
            RectangleEdge rangeEdge = getDomainAxisEdge();

            SeriesRenderingOrder seriesOrder = getSeriesRenderingOrder();
            if (seriesOrder == SeriesRenderingOrder.REVERSE) {
                //render series in reverse order
                for (int pass = 0; pass < passCount; pass++) {
                    int seriesCount = dataset.getSeriesCount();
                    for (int series = seriesCount - 1; series >= 0; series--) {
                        int firstItem = 0;
                        int lastItem = dataset.getItemCount(series) - 1;
                        if (lastItem == -1) {
                            continue;
                        }
                        if (state.getProcessVisibleItemsOnly()) {
                            int[] itemBounds = RendererUtilities.findLiveItems(
                                    dataset, series, xAxis.getLowerBound(),
                                    xAxis.getUpperBound());
                            firstItem = itemBounds[0];
                            lastItem = itemBounds[1];
                        }
                        for (int item = firstItem; item <= lastItem; item++) {
                            if (!hasRendered(dataset, xAxis, yAxis, domainEdge, rangeEdge, dataArea, series, item)) {
                                renderer.drawItem(g2, state, dataArea, info,
                                        this, xAxis, yAxis, dataset, series, item,
                                        crosshairState, pass);
                            }
                        }
                    }
                }
            } else {
                //render series in forward order
                for (int pass = 0; pass < passCount; pass++) {
                    int seriesCount = dataset.getSeriesCount();
                    for (int series = 0; series < seriesCount; series++) {
                        int firstItem = 0;
                        int lastItem = dataset.getItemCount(series) - 1;
                        if (state.getProcessVisibleItemsOnly()) {
                            int[] itemBounds = RendererUtilities.findLiveItems(
                                    dataset, series, xAxis.getLowerBound(),
                                    xAxis.getUpperBound());
                            firstItem = itemBounds[0];
                            lastItem = itemBounds[1];
                        }
                        for (int item = firstItem; item <= lastItem; item++) {
                            if (!hasRendered(dataset, xAxis, yAxis, domainEdge, rangeEdge, dataArea, series, item)) {
                                renderer.drawItem(g2, state, dataArea, info,
                                        this, xAxis, yAxis, dataset, series, item,
                                        crosshairState, pass);
                            }
                        }
                    }
                }
            }
        }
        return foundData;
    }

}


gumshoe
Posts: 80
Joined: Mon Jul 12, 2004 6:06 pm

Post by gumshoe » Sun Aug 05, 2007 4:08 am

Good to see others having, and solving, problems with massive data sets. My requirements are for 500K datasets in XYPlots drawn in ~5 seconds on a P4 3GHz box. I achieved this in 2005 (1.0.1 days), with the help of the folks in this thread: http://www.jfree.org/phpBB2/viewtopic.p ... 0&start=30.

We modified drawItem() in StandardXYItemRenderer to do just what you mentioned; only execute a g2.draw() if the draw would not be over top of a previous one. Sadly these efforts have not made it into any subsequent release (even 1.2.0-pre1). I just upgraded to 1.0.6 and re-implemented the changes to StandardXYItemRenderer.

The changes are minor and I did not extend the class on account of visibility constraints.

Code: Select all

package org.jfree.chart.renderer.xy;


import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Paint;
import java.awt.Point;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

import org.jfree.chart.LegendItem;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.entity.EntityCollection;
import org.jfree.chart.event.RendererChangeEvent;
import org.jfree.chart.labels.XYToolTipGenerator;
import org.jfree.chart.plot.CrosshairState;
import org.jfree.chart.plot.Plot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.urls.XYURLGenerator;
import org.jfree.data.xy.XYDataset;
import org.jfree.io.SerialUtilities;
import org.jfree.ui.RectangleEdge;
import org.jfree.util.BooleanList;
import org.jfree.util.BooleanUtilities;
import org.jfree.util.ObjectUtilities;
import org.jfree.util.PublicCloneable;
import org.jfree.util.ShapeUtilities;
import org.jfree.util.UnitType;

/**
 * Standard item renderer for an {@link XYPlot}.  This class can draw (a) 
 * shapes at each point, or (b) lines between points, or (c) both shapes and 
 * lines.
 * <P>
 * This renderer has been retained for historical reasons and, in general, you
 * should use the {@link XYLineAndShapeRenderer} class instead.
 */
public class StandardXYItemRenderer extends AbstractXYItemRenderer 
        implements XYItemRenderer, Cloneable, PublicCloneable, Serializable {

    /** For serialization. */
    private static final long serialVersionUID = -3271351259436865995L;
    
    /** Constant for the type of rendering (shapes only). */
    public static final int SHAPES = 1;

    /** Constant for the type of rendering (lines only). */
    public static final int LINES = 2;

    /** Constant for the type of rendering (shapes and lines). */
    public static final int SHAPES_AND_LINES = SHAPES | LINES;

    /** Constant for the type of rendering (images only). */
    public static final int IMAGES = 4;

    /** Constant for the type of rendering (discontinuous lines). */
    public static final int DISCONTINUOUS = 8;

    /** Constant for the type of rendering (discontinuous lines). */
    public static final int DISCONTINUOUS_LINES = LINES | DISCONTINUOUS;

    /** A flag indicating whether or not shapes are drawn at each XY point. */
    private boolean baseShapesVisible;

    /** A flag indicating whether or not lines are drawn between XY points. */
    private boolean plotLines;

    /** A flag indicating whether or not images are drawn between XY points. */
    private boolean plotImages;

    /** A flag controlling whether or not discontinuous lines are used. */
    private boolean plotDiscontinuous;

    /** Specifies how the gap threshold value is interpreted. */
    private UnitType gapThresholdType = UnitType.RELATIVE;
    
    /** Threshold for deciding when to discontinue a line. */
    private double gapThreshold = 1.0;

    /** A flag that controls whether or not shapes are filled for ALL series. */
    private Boolean shapesFilled;

    /** 
     * A table of flags that control (per series) whether or not shapes are 
     * filled. 
     */
    private BooleanList seriesShapesFilled;

    /** The default value returned by the getShapeFilled() method. */
    private boolean baseShapesFilled;

    /** 
     * A flag that controls whether or not each series is drawn as a single 
     * path. 
     */
    private boolean drawSeriesLineAsPath;

    /** 
     * The shape that is used to represent a line in the legend. 
     * This should never be set to <code>null</code>. 
     */
    private transient Shape legendLine;
    
    /** A counter to prevent unnecessary Graphics2D.draw() events in drawItem() */
    private int previousDrawnItem;
    private int skippedDrawItems;
    
    /**
     * Constructs a new renderer.
     */
    public StandardXYItemRenderer() {
        this(LINES, null);
    }

    /**
     * Constructs a new renderer.  To specify the type of renderer, use one of 
     * the constants: {@link #SHAPES}, {@link #LINES} or 
     * {@link #SHAPES_AND_LINES}.
     *
     * @param type  the type.
     */
    public StandardXYItemRenderer(int type) {
        this(type, null);
    }

    /**
     * Constructs a new renderer.  To specify the type of renderer, use one of 
     * the constants: {@link #SHAPES}, {@link #LINES} or 
     * {@link #SHAPES_AND_LINES}.
     *
     * @param type  the type of renderer.
     * @param toolTipGenerator  the item label generator (<code>null</code> 
     *                          permitted).
     */
    public StandardXYItemRenderer(int type, 
                                  XYToolTipGenerator toolTipGenerator) {
        this(type, toolTipGenerator, null);
    }

    /**
     * Constructs a new renderer.  To specify the type of renderer, use one of 
     * the constants: {@link #SHAPES}, {@link #LINES} or 
     * {@link #SHAPES_AND_LINES}.
     *
     * @param type  the type of renderer.
     * @param toolTipGenerator  the item label generator (<code>null</code> 
     *                          permitted).
     * @param urlGenerator  the URL generator.
     */
    public StandardXYItemRenderer(int type,
                                  XYToolTipGenerator toolTipGenerator,
                                  XYURLGenerator urlGenerator) {

        super();
        setBaseToolTipGenerator(toolTipGenerator);
        setURLGenerator(urlGenerator);
        if ((type & SHAPES) != 0) {
            this.baseShapesVisible = true;
        }
        if ((type & LINES) != 0) {
            this.plotLines = true;
        }
        if ((type & IMAGES) != 0) {
            this.plotImages = true;
        }
        if ((type & DISCONTINUOUS) != 0) {
            this.plotDiscontinuous = true;
        }

        this.shapesFilled = null;
        this.seriesShapesFilled = new BooleanList();
        this.baseShapesFilled = true;
        this.legendLine = new Line2D.Double(-7.0, 0.0, 7.0, 0.0);
        this.drawSeriesLineAsPath = false;
    }

    /**
     * Returns true if shapes are being plotted by the renderer.
     *
     * @return <code>true</code> if shapes are being plotted by the renderer.
     * 
     * @see #setBaseShapesVisible
     */
    public boolean getBaseShapesVisible() {
        return this.baseShapesVisible;
    }

    /**
     * Sets the flag that controls whether or not a shape is plotted at each 
     * data point.
     *
     * @param flag  the flag.
     * 
     * @see #getBaseShapesVisible
     */
    public void setBaseShapesVisible(boolean flag) {
        if (this.baseShapesVisible != flag) {
            this.baseShapesVisible = flag;
            notifyListeners(new RendererChangeEvent(this));
        }
    }

    // SHAPES FILLED

    /**
     * Returns the flag used to control whether or not the shape for an item is
     * filled.
     * <p>
     * The default implementation passes control to the 
     * <code>getSeriesShapesFilled</code> method.  You can override this method 
     * if you require different behaviour.
     *
     * @param series  the series index (zero-based).
     * @param item  the item index (zero-based).
     *
     * @return A boolean.
     * 
     * @see #getSeriesShapesFilled(int)
     */
    public boolean getItemShapeFilled(int series, int item) {
        // return the overall setting, if there is one...
        if (this.shapesFilled != null) {
            return this.shapesFilled.booleanValue();
        }

        // otherwise look up the paint table
        Boolean flag = this.seriesShapesFilled.getBoolean(series);
        if (flag != null) {
            return flag.booleanValue();
        }
        else {
            return this.baseShapesFilled;
        }
    }

    /**
     * Returns the override flag that controls whether or not shapes are filled
     * for ALL series.
     * 
     * @return The flag (possibly <code>null</code>).
     * 
     * @since 1.0.5
     */
    public Boolean getShapesFilled() {
        return this.shapesFilled;
    }
    
    /**
     * Sets the 'shapes filled' for ALL series.
     *
     * @param filled  the flag.
     * 
     * @see #setShapesFilled(Boolean)
     */
    public void setShapesFilled(boolean filled) {
        // here we use BooleanUtilities to remain compatible with JDKs < 1.4 
        setShapesFilled(BooleanUtilities.valueOf(filled));
    }

    /**
     * Sets the override flag that controls whether or not shapes are filled
     * for ALL series and sends a {@link RendererChangeEvent} to all registered
     * listeners. 
     *
     * @param filled  the flag (<code>null</code> permitted).
     * 
     * @see #setShapesFilled(boolean)
     */
    public void setShapesFilled(Boolean filled) {
        this.shapesFilled = filled;
        fireChangeEvent();
    }

    /**
     * Returns the flag used to control whether or not the shapes for a series
     * are filled.
     *
     * @param series  the series index (zero-based).
     *
     * @return A boolean.
     */
    public Boolean getSeriesShapesFilled(int series) {
        return this.seriesShapesFilled.getBoolean(series);
    }

    /**
     * Sets the 'shapes filled' flag for a series.
     *
     * @param series  the series index (zero-based).
     * @param flag  the flag.
     * 
     * @see #getSeriesShapesFilled(int)
     */
    public void setSeriesShapesFilled(int series, Boolean flag) {
        this.seriesShapesFilled.setBoolean(series, flag);
        fireChangeEvent();
    }

    /**
     * Returns the base 'shape filled' attribute.
     *
     * @return The base flag.
     * 
     * @see #setBaseShapesFilled(boolean)
     */
    public boolean getBaseShapesFilled() {
        return this.baseShapesFilled;
    }

    /**
     * Sets the base 'shapes filled' flag.
     *
     * @param flag  the flag.
     * 
     * @see #getBaseShapesFilled()
     */
    public void setBaseShapesFilled(boolean flag) {
        this.baseShapesFilled = flag;
    }

    /**
     * Returns true if lines are being plotted by the renderer.
     *
     * @return <code>true</code> if lines are being plotted by the renderer.
     * 
     * @see #setPlotLines(boolean)
     */
    public boolean getPlotLines() {
        return this.plotLines;
    }

    /**
     * Sets the flag that controls whether or not a line is plotted between 
     * each data point.
     *
     * @param flag  the flag.
     * 
     * @see #getPlotLines()
     */
    public void setPlotLines(boolean flag) {
        if (this.plotLines != flag) {
            this.plotLines = flag;
            notifyListeners(new RendererChangeEvent(this));
        }
    }

    /**
     * Returns the gap threshold type (relative or absolute).
     * 
     * @return The type.
     * 
     * @see #setGapThresholdType(UnitType)
     */
    public UnitType getGapThresholdType() {
        return this.gapThresholdType;
    }
    
    /**
     * Sets the gap threshold type.
     * 
     * @param thresholdType  the type (<code>null</code> not permitted).
     * 
     * @see #getGapThresholdType()
     */
    public void setGapThresholdType(UnitType thresholdType) {
        if (thresholdType == null) {
            throw new IllegalArgumentException(
                    "Null 'thresholdType' argument.");
        }
        this.gapThresholdType = thresholdType;
        notifyListeners(new RendererChangeEvent(this));
    }
    
    /**
     * Returns the gap threshold for discontinuous lines.
     *
     * @return The gap threshold.
     * 
     * @see #setGapThreshold(double)
     */
    public double getGapThreshold() {
        return this.gapThreshold;
    }

    /**
     * Sets the gap threshold for discontinuous lines.
     *
     * @param t  the threshold.
     * 
     * @see #getGapThreshold()
     */
    public void setGapThreshold(double t) {
        this.gapThreshold = t;
        notifyListeners(new RendererChangeEvent(this));
    }

    /**
     * Returns true if images are being plotted by the renderer.
     *
     * @return <code>true</code> if images are being plotted by the renderer.
     * 
     * @see #setPlotImages(boolean)
     */
    public boolean getPlotImages() {
        return this.plotImages;
    }

    /**
     * Sets the flag that controls whether or not an image is drawn at each 
     * data point.
     *
     * @param flag  the flag.
     * 
     * @see #getPlotImages()
     */
    public void setPlotImages(boolean flag) {
        if (this.plotImages != flag) {
            this.plotImages = flag;
            notifyListeners(new RendererChangeEvent(this));
        }
    }

    /**
     * Returns a flag that controls whether or not the renderer shows
     * discontinuous lines.
     *
     * @return <code>true</code> if lines should be discontinuous.
     */
    public boolean getPlotDiscontinuous() {
        return this.plotDiscontinuous;
    }
    
    /**
     * Sets the flag that controls whether or not the renderer shows
     * discontinuous lines, and sends a {@link RendererChangeEvent} to all
     * registered listeners.
     * 
     * @param flag  the new flag value.
     * 
     * @since 1.0.5
     */
    public void setPlotDiscontinuous(boolean flag) {
        if (this.plotDiscontinuous != flag) {
            this.plotDiscontinuous = flag;
            fireChangeEvent();
        }
    }

    /**
     * Returns a flag that controls whether or not each series is drawn as a 
     * single path.
     * 
     * @return A boolean.
     * 
     * @see #setDrawSeriesLineAsPath(boolean)
     */
    public boolean getDrawSeriesLineAsPath() {
        return this.drawSeriesLineAsPath;
    }
    
    /**
     * Sets the flag that controls whether or not each series is drawn as a 
     * single path.
     * 
     * @param flag  the flag.
     * 
     * @see #getDrawSeriesLineAsPath()
     */
    public void setDrawSeriesLineAsPath(boolean flag) {
        this.drawSeriesLineAsPath = flag;
    }
    
    /**
     * Returns the shape used to represent a line in the legend.
     * 
     * @return The legend line (never <code>null</code>).
     * 
     * @see #setLegendLine(Shape)
     */
    public Shape getLegendLine() {
        return this.legendLine;   
    }
    
    /**
     * Sets the shape used as a line in each legend item and sends a 
     * {@link RendererChangeEvent} to all registered listeners.
     * 
     * @param line  the line (<code>null</code> not permitted).
     * 
     * @see #getLegendLine()
     */
    public void setLegendLine(Shape line) {
        if (line == null) {
            throw new IllegalArgumentException("Null 'line' argument.");   
        }
        this.legendLine = line;
        notifyListeners(new RendererChangeEvent(this));
    }

    /**
     * Returns a legend item for a series.
     *
     * @param datasetIndex  the dataset index (zero-based).
     * @param series  the series index (zero-based).
     *
     * @return A legend item for the series.
     */
    public LegendItem getLegendItem(int datasetIndex, int series) {
        XYPlot plot = getPlot();
        if (plot == null) {
            return null;
        }
        LegendItem result = null;
        XYDataset dataset = plot.getDataset(datasetIndex);
        if (dataset != null) {
            if (getItemVisible(series, 0)) {
                String label = getLegendItemLabelGenerator().generateLabel(
                        dataset, series);
                String description = label;
                String toolTipText = null;
                if (getLegendItemToolTipGenerator() != null) {
                    toolTipText = getLegendItemToolTipGenerator().generateLabel(
                            dataset, series);
                }
                String urlText = null;
                if (getLegendItemURLGenerator() != null) {
                    urlText = getLegendItemURLGenerator().generateLabel(
                            dataset, series);
                }
                Shape shape = lookupSeriesShape(series);
                boolean shapeFilled = getItemShapeFilled(series, 0);
                Paint paint = lookupSeriesPaint(series);
                Paint linePaint = paint;
                Stroke lineStroke = lookupSeriesStroke(series);
                result = new LegendItem(label, description, toolTipText, 
                        urlText, this.baseShapesVisible, shape, shapeFilled,
                        paint, !shapeFilled, paint, lineStroke, 
                        this.plotLines, this.legendLine, lineStroke, linePaint);
                result.setDataset(dataset);
                result.setDatasetIndex(datasetIndex);
                result.setSeriesKey(dataset.getSeriesKey(series));
                result.setSeriesIndex(series);
            }
        }
        return result;
    }

    /**
     * Records the state for the renderer.  This is used to preserve state 
     * information between calls to the drawItem() method for a single chart 
     * drawing.
     */
    public static class State extends XYItemRendererState {
        
        /** The path for the current series. */
        public GeneralPath seriesPath;
        
        /** The series index. */
        private int seriesIndex;
        
        /** 
         * A flag that indicates if the last (x, y) point was 'good' 
         * (non-null). 
         */
        private boolean lastPointGood;
        
        /**
         * Creates a new state instance.
         * 
         * @param info  the plot rendering info.
         */
        public State(PlotRenderingInfo info) {
            super(info);
        }
        
        /**
         * Returns a flag that indicates if the last point drawn (in the 
         * current series) was 'good' (non-null).
         * 
         * @return A boolean.
         */
        public boolean isLastPointGood() {
            return this.lastPointGood;
        }
        
        /**
         * Sets a flag that indicates if the last point drawn (in the current 
         * series) was 'good' (non-null).
         * 
         * @param good  the flag.
         */
        public void setLastPointGood(boolean good) {
            this.lastPointGood = good;
        }
        
        /**
         * Returns the series index for the current path.
         * 
         * @return The series index for the current path.
         */
        public int getSeriesIndex() {
            return this.seriesIndex;
        }
        
        /**
         * Sets the series index for the current path.
         * 
         * @param index  the index.
         */
        public void setSeriesIndex(int index) {
            this.seriesIndex = index;
        }
    }
    
    /**
     * Initialises the renderer.
     * <P>
     * This method will be called before the first item is rendered, giving the
     * renderer an opportunity to initialise any state information it wants to 
     * maintain. The renderer can do nothing if it chooses.
     *
     * @param g2  the graphics device.
     * @param dataArea  the area inside the axes.
     * @param plot  the plot.
     * @param data  the data.
     * @param info  an optional info collection object to return data back to 
     *              the caller.
     *
     * @return The renderer state.
     */
    public XYItemRendererState initialise(Graphics2D g2,
                                          Rectangle2D dataArea,
                                          XYPlot plot,
                                          XYDataset data,
                                          PlotRenderingInfo info) {

        State state = new State(info);
        state.seriesPath = new GeneralPath();
        state.seriesIndex = -1;
        return state;

    }
    
    /**
     * Draws the visual representation of a single data item.
     *
     * @param g2  the graphics device.
     * @param state  the renderer state.
     * @param dataArea  the area within which the data is being drawn.
     * @param info  collects information about the drawing.
     * @param plot  the plot (can be used to obtain standard color information 
     *              etc).
     * @param domainAxis  the domain axis.
     * @param rangeAxis  the range axis.
     * @param dataset  the dataset.
     * @param series  the series index (zero-based).
     * @param item  the item index (zero-based).
     * @param crosshairState  crosshair information for the plot 
     *                        (<code>null</code> permitted).
     * @param pass  the pass index.
     */
    public void drawItem(Graphics2D g2, 
                         XYItemRendererState state,
                         Rectangle2D dataArea, 
                         PlotRenderingInfo info, 
                         XYPlot plot,
                         ValueAxis domainAxis, 
                         ValueAxis rangeAxis, 
                         XYDataset dataset,
                         int series, 
                         int item, 
                         CrosshairState crosshairState, 
                         int pass) {

        boolean itemVisible = getItemVisible(series, item);
        
        // setup for collecting optional entity info...
        boolean bAddEntity=false;
        Shape entityArea = null;
        EntityCollection entities = null;
        if (info != null) {
            entities = info.getOwner().getEntityCollection();
        }

        PlotOrientation orientation = plot.getOrientation();
        Paint paint = getItemPaint(series, item);
        Stroke seriesStroke = getItemStroke(series, item);
        g2.setPaint(paint);
        g2.setStroke(seriesStroke);

        // get the data point...
        double x1 = dataset.getXValue(series, item);
        double y1 = dataset.getYValue(series, item);
        if (Double.isNaN(x1) || Double.isNaN(y1)) {
            itemVisible = false;
        }

        RectangleEdge xAxisLocation = plot.getDomainAxisEdge();
        RectangleEdge yAxisLocation = plot.getRangeAxisEdge();
        double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation);
        double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation);

        if (getPlotLines()) {
            if (this.drawSeriesLineAsPath) {
                State s = (State) state;
                if (s.getSeriesIndex() != series) {
                    // we are starting a new series path
                    s.seriesPath.reset();
                    s.lastPointGood = false;
                    s.setSeriesIndex(series);
                }
                if(0 == item) {
                    this.previousDrawnItem = 0;
                }
                
                // update path to reflect latest point
                if (itemVisible && !Double.isNaN(transX1) 
                        && !Double.isNaN(transY1)) {
                    float x = (float) transX1;
                    float y = (float) transY1;
                    if (orientation == PlotOrientation.HORIZONTAL) {
                        x = (float) transY1;
                        y = (float) transX1;
                    }
                    if (s.isLastPointGood()) {
                        // TODO: check threshold
                        s.seriesPath.lineTo(x, y);
                    }
                    else {
                        s.seriesPath.moveTo(x, y);
                    }
                    s.setLastPointGood(true);
                }
                else {
                    s.setLastPointGood(false);
                }
                if (item == dataset.getItemCount(series) - 1) {
                    if (s.seriesIndex == series) {
                        // draw path
                        g2.setStroke(lookupSeriesStroke(series));
                        g2.setPaint(lookupSeriesPaint(series));
                        g2.draw(s.seriesPath);
                    }
                }
            }
            else if (item != 0 && itemVisible) {
		            // get the previous data point...
		            int idx = item - this.previousDrawnItem;
		            if(idx < 0)
		            {
		              //there exists some confusion; do not draw anything and reset state
		              this.previousDrawnItem = 0;
		            }
		            else
		            {
		                // get the previous data point...
		                double x0 = dataset.getXValue(series, idx);
		                double y0 = dataset.getYValue(series, idx);
		                if (!Double.isNaN(x0) && !Double.isNaN(y0)) {
		                    boolean drawLine = true;
		                    if (getPlotDiscontinuous()) {
		                        // only draw a line if the gap between the current and 
		                        // previous data point is within the threshold
		                        int numX = dataset.getItemCount(series);
		                        double minX = dataset.getXValue(series, 0);
		                        double maxX = dataset.getXValue(series, numX - 1);
		                        if (this.gapThresholdType == UnitType.ABSOLUTE) {
		                            drawLine = Math.abs(x1 - x0) <= this.gapThreshold;
		                        }
		                        else {
		                            drawLine = Math.abs(x1 - x0) <= ((maxX - minX) 
		                                / numX * getGapThreshold());
		                        }
		                    }
		                    if (drawLine) {
		                        double transX0 = domainAxis.valueToJava2D(x0, dataArea,
		                                xAxisLocation);
		                        double transY0 = rangeAxis.valueToJava2D(y0, dataArea, 
		                                yAxisLocation);
		
		                        // only draw if we have good values
		                        if (Double.isNaN(transX0) || Double.isNaN(transY0) 
		                            || Double.isNaN(transX1) || Double.isNaN(transY1)) {
		                            return;
		                        }
		
		                        // Only draw line if it is more than a pixel away from the previous one
		                        if((0 == this.previousDrawnItem || transX1 - transX0 > 2 || transX1 - transX0 < -2 || transY1 - transY0 > 2 
		                            || transY1 - transY0 < -2)) {
				                        this.previousDrawnItem = 1;
				                        if (orientation == PlotOrientation.HORIZONTAL) {
				                            state.workingLine.setLine(transY0, transX0, 
				                                    transY1, transX1);
				                        }
				                        else if (orientation == PlotOrientation.VERTICAL) {
				                            state.workingLine.setLine(transX0, transY0, 
				                                    transX1, transY1);
				                        }
				
				                        if (state.workingLine.intersects(dataArea)) {
				                            g2.draw(state.workingLine);
				                        }
			                      }
		                        else {
		                            //Increase counter for the previous drawn item.
		                            previousDrawnItem++; 
		                            bAddEntity=false;
		                            this.skippedDrawItems++;
		                        }
		                    }
		                }
		            }
            }
        }
        
        // we needed to get this far even for invisible items, to ensure that
        // seriesPath updates happened, but now there is nothing more we need
        // to do for non-visible items...
        if (!itemVisible) {
            return;
        }

        if (getBaseShapesVisible()) {

            Shape shape = getItemShape(series, item);
            if (orientation == PlotOrientation.HORIZONTAL) {
                shape = ShapeUtilities.createTranslatedShape(shape, transY1, 
                        transX1);
            }
            else if (orientation == PlotOrientation.VERTICAL) {
                shape = ShapeUtilities.createTranslatedShape(shape, transX1, 
                        transY1);
            }
            if (shape.intersects(dataArea)) {
                bAddEntity=true;
                if (getItemShapeFilled(series, item)) {
                    g2.fill(shape);
                }
                else {
                    g2.draw(shape);
                }
            }
            entityArea = shape;

        }

        if (getPlotImages()) {
            Image image = getImage(plot, series, item, transX1, transY1);
            if (image != null) {
                Point hotspot = getImageHotspot(plot, series, item, transX1, 
                        transY1, image);
                g2.drawImage(image, (int) (transX1 - hotspot.getX()), 
                        (int) (transY1 - hotspot.getY()), null);
                entityArea = new Rectangle2D.Double(transX1 - hotspot.getX(), 
                        transY1 - hotspot.getY(), image.getWidth(null), 
                        image.getHeight(null));
            }

        }

        double xx = transX1;
        double yy = transY1;
        if (orientation == PlotOrientation.HORIZONTAL) {
            xx = transY1;
            yy = transX1;
        }          

        // draw the item label if there is one...
        if (isItemLabelVisible(series, item)) {
            drawItemLabel(g2, orientation, dataset, series, item, xx, yy, 
                    (y1 < 0.0));
        }

        int domainAxisIndex = plot.getDomainAxisIndex(domainAxis);
        int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis);
        updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex, 
                rangeAxisIndex, transX1, transY1, orientation);

        // add an entity for the item...
        if (entities != null && dataArea.contains(xx, yy) && bAddEntity) {
            addEntity(entities, entityArea, dataset, series, item, xx, yy);
        }

    }

    /**
     * Tests this renderer for equality with another object.
     *
     * @param obj  the object (<code>null</code> permitted).
     *
     * @return A boolean.
     */
    public boolean equals(Object obj) {

        if (obj == this) {
            return true;
        }
        if (!(obj instanceof StandardXYItemRenderer)) {
            return false;
        }
        StandardXYItemRenderer that = (StandardXYItemRenderer) obj;
        if (this.baseShapesVisible != that.baseShapesVisible) {
            return false;
        }
        if (this.plotLines != that.plotLines) {
            return false;
        }
        if (this.plotImages != that.plotImages) {
            return false;
        }
        if (this.plotDiscontinuous != that.plotDiscontinuous) {
            return false;
        }
        if (this.gapThresholdType != that.gapThresholdType) {
            return false;
        }
        if (this.gapThreshold != that.gapThreshold) {
            return false;
        }
        if (!ObjectUtilities.equal(this.shapesFilled, that.shapesFilled)) {
            return false;
        }
        if (!this.seriesShapesFilled.equals(that.seriesShapesFilled)) {
            return false;
        }
        if (this.baseShapesFilled != that.baseShapesFilled) {
            return false;
        }
        if (this.drawSeriesLineAsPath != that.drawSeriesLineAsPath) {
            return false;
        }
        if (!ShapeUtilities.equal(this.legendLine, that.legendLine)) {
            return false;   
        }
        return super.equals(obj);

    }

    /**
     * Returns a clone of the renderer.
     *
     * @return A clone.
     *
     * @throws CloneNotSupportedException  if the renderer cannot be cloned.
     */
    public Object clone() throws CloneNotSupportedException {
        StandardXYItemRenderer clone = (StandardXYItemRenderer) super.clone();
        clone.seriesShapesFilled 
                = (BooleanList) this.seriesShapesFilled.clone();
        clone.legendLine = ShapeUtilities.clone(this.legendLine);
        return clone;
    }

    ////////////////////////////////////////////////////////////////////////////
    // PROTECTED METHODS
    // These provide the opportunity to subclass the standard renderer and 
    // create custom effects.
    ////////////////////////////////////////////////////////////////////////////

    /**
     * Returns the image used to draw a single data item.
     *
     * @param plot  the plot (can be used to obtain standard color information 
     *              etc).
     * @param series  the series index.
     * @param item  the item index.
     * @param x  the x value of the item.
     * @param y  the y value of the item.
     *
     * @return The image.
     * 
     * @see #getPlotImages()
     */
    protected Image getImage(Plot plot, int series, int item, 
                             double x, double y) {
        // this method must be overridden if you want to display images
        return null;
    }

    /**
     * Returns the hotspot of the image used to draw a single data item.
     * The hotspot is the point relative to the top left of the image
     * that should indicate the data item. The default is the center of the
     * image.
     *
     * @param plot  the plot (can be used to obtain standard color information 
     *              etc).
     * @param image  the image (can be used to get size information about the 
     *               image)
     * @param series  the series index
     * @param item  the item index
     * @param x  the x value of the item
     * @param y  the y value of the item
     *
     * @return The hotspot used to draw the data item.
     */
    protected Point getImageHotspot(Plot plot, int series, int item,
                                    double x, double y, Image image) {

        int height = image.getHeight(null);
        int width = image.getWidth(null);
        return new Point(width / 2, height / 2);

    }
    protected void finalize() throws Throwable
    {
      System.out.println("skipped draw items: "+this.skippedDrawItems);
    }
    
    /**
     * Provides serialization support.
     *
     * @param stream  the input stream.
     *
     * @throws IOException  if there is an I/O error.
     * @throws ClassNotFoundException  if there is a classpath problem.
     */
    private void readObject(ObjectInputStream stream) 
            throws IOException, ClassNotFoundException {
        stream.defaultReadObject();
        this.legendLine = SerialUtilities.readShape(stream);
    }
    
    /**
     * Provides serialization support.
     *
     * @param stream  the output stream.
     *
     * @throws IOException  if there is an I/O error.
     */
    private void writeObject(ObjectOutputStream stream) throws IOException {
        stream.defaultWriteObject();
        SerialUtilities.writeShape(this.legendLine, stream);
    }

}
For an overview, here is the diff to revision 1 of https://jfreechart.svn.sourceforge.net/ ... jfreechart

Code: Select all

Index: E:/work/eclipse3.2workspace/JFreeChart/source/org/jfree/chart/renderer/xy/StandardXYItemRenderer.java
===================================================================
--- E:/work/eclipse3.2workspace/JFreeChart/source/org/jfree/chart/renderer/xy/StandardXYItemRenderer.java	(revision 1)
+++ E:/work/eclipse3.2workspace/JFreeChart/source/org/jfree/chart/renderer/xy/StandardXYItemRenderer.java	(working copy)
@@ -218,6 +218,10 @@
      */
     private transient Shape legendLine;
     
+    /** A counter to prevent unnecessary Graphics2D.draw() events in drawItem() */
+    private int previousDrawnItem;
+    private int skippedDrawItems;
+    
     /**
      * Constructs a new renderer.
      */
@@ -788,6 +792,7 @@
         boolean itemVisible = getItemVisible(series, item);
         
         // setup for collecting optional entity info...
+        boolean bAddEntity=false;
         Shape entityArea = null;
         EntityCollection entities = null;
         if (info != null) {
@@ -821,6 +826,9 @@
                     s.lastPointGood = false;
                     s.setSeriesIndex(series);
                 }
+                if(0 == item) {
+                    this.previousDrawnItem = 0;
+                }
                 
                 // update path to reflect latest point
                 if (itemVisible && !Double.isNaN(transX1) 
@@ -852,53 +860,73 @@
                     }
                 }
             }
-
             else if (item != 0 && itemVisible) {
-                // get the previous data point...
-                double x0 = dataset.getXValue(series, item - 1);
-                double y0 = dataset.getYValue(series, item - 1);
-                if (!Double.isNaN(x0) && !Double.isNaN(y0)) {
-                    boolean drawLine = true;
-                    if (getPlotDiscontinuous()) {
-                        // only draw a line if the gap between the current and 
-                        // previous data point is within the threshold
-                        int numX = dataset.getItemCount(series);
-                        double minX = dataset.getXValue(series, 0);
-                        double maxX = dataset.getXValue(series, numX - 1);
-                        if (this.gapThresholdType == UnitType.ABSOLUTE) {
-                            drawLine = Math.abs(x1 - x0) <= this.gapThreshold;
-                        }
-                        else {
-                            drawLine = Math.abs(x1 - x0) <= ((maxX - minX) 
-                                / numX * getGapThreshold());
-                        }
-                    }
-                    if (drawLine) {
-                        double transX0 = domainAxis.valueToJava2D(x0, dataArea,
-                                xAxisLocation);
-                        double transY0 = rangeAxis.valueToJava2D(y0, dataArea, 
-                                yAxisLocation);
-
-                        // only draw if we have good values
-                        if (Double.isNaN(transX0) || Double.isNaN(transY0) 
-                            || Double.isNaN(transX1) || Double.isNaN(transY1)) {
-                            return;
-                        }
-
-                        if (orientation == PlotOrientation.HORIZONTAL) {
-                            state.workingLine.setLine(transY0, transX0, 
-                                    transY1, transX1);
-                        }
-                        else if (orientation == PlotOrientation.VERTICAL) {
-                            state.workingLine.setLine(transX0, transY0, 
-                                    transX1, transY1);
-                        }
-
-                        if (state.workingLine.intersects(dataArea)) {
-                            g2.draw(state.workingLine);
-                        }
-                    }
-                }
+		            // get the previous data point...
+		            int idx = item - this.previousDrawnItem;
+		            if(idx < 0)
+		            {
+		              //there exists some confusion; do not draw anything and reset state
+		              this.previousDrawnItem = 0;
+		            }
+		            else
+		            {
+		                // get the previous data point...
+		                double x0 = dataset.getXValue(series, idx);
+		                double y0 = dataset.getYValue(series, idx);
+		                if (!Double.isNaN(x0) && !Double.isNaN(y0)) {
+		                    boolean drawLine = true;
+		                    if (getPlotDiscontinuous()) {
+		                        // only draw a line if the gap between the current and 
+		                        // previous data point is within the threshold
+		                        int numX = dataset.getItemCount(series);
+		                        double minX = dataset.getXValue(series, 0);
+		                        double maxX = dataset.getXValue(series, numX - 1);
+		                        if (this.gapThresholdType == UnitType.ABSOLUTE) {
+		                            drawLine = Math.abs(x1 - x0) <= this.gapThreshold;
+		                        }
+		                        else {
+		                            drawLine = Math.abs(x1 - x0) <= ((maxX - minX) 
+		                                / numX * getGapThreshold());
+		                        }
+		                    }
+		                    if (drawLine) {
+		                        double transX0 = domainAxis.valueToJava2D(x0, dataArea,
+		                                xAxisLocation);
+		                        double transY0 = rangeAxis.valueToJava2D(y0, dataArea, 
+		                                yAxisLocation);
+		
+		                        // only draw if we have good values
+		                        if (Double.isNaN(transX0) || Double.isNaN(transY0) 
+		                            || Double.isNaN(transX1) || Double.isNaN(transY1)) {
+		                            return;
+		                        }
+		
+		                        // Only draw line if it is more than a pixel away from the previous one
+		                        if((0 == this.previousDrawnItem || transX1 - transX0 > 2 || transX1 - transX0 < -2 || transY1 - transY0 > 2 
+		                            || transY1 - transY0 < -2)) {
+				                        this.previousDrawnItem = 1;
+				                        if (orientation == PlotOrientation.HORIZONTAL) {
+				                            state.workingLine.setLine(transY0, transX0, 
+				                                    transY1, transX1);
+				                        }
+				                        else if (orientation == PlotOrientation.VERTICAL) {
+				                            state.workingLine.setLine(transX0, transY0, 
+				                                    transX1, transY1);
+				                        }
+				
+				                        if (state.workingLine.intersects(dataArea)) {
+				                            g2.draw(state.workingLine);
+				                        }
+			                      }
+		                        else {
+		                            //Increase counter for the previous drawn item.
+		                            previousDrawnItem++; 
+		                            bAddEntity=false;
+		                            this.skippedDrawItems++;
+		                        }
+		                    }
+		                }
+		            }
             }
         }
         
@@ -921,6 +949,7 @@
                         transY1);
             }
             if (shape.intersects(dataArea)) {
+                bAddEntity=true;
                 if (getItemShapeFilled(series, item)) {
                     g2.fill(shape);
                 }
@@ -965,7 +994,7 @@
                 rangeAxisIndex, transX1, transY1, orientation);
 
         // add an entity for the item...
-        if (entities != null && dataArea.contains(xx, yy)) {
+        if (entities != null && dataArea.contains(xx, yy) && bAddEntity) {
             addEntity(entities, entityArea, dataset, series, item, xx, yy);
         }
 
@@ -1090,6 +1119,10 @@
         return new Point(width / 2, height / 2);
 
     }
+    protected void finalize() throws Throwable
+    {
+      System.out.println("skipped draw items: "+this.skippedDrawItems);
+    }
     
     /**
      * Provides serialization support.
I have not tried your solution nonlinear5, I expect to soon though.

Comments?

gumshoe
Posts: 80
Joined: Mon Jul 12, 2004 6:06 pm

Post by gumshoe » Sun Aug 05, 2007 4:40 am

Just tried the FastXYPlot and am liking it. My completely scientific and reproducible tests (thank you second hand) show it to be as fast or faster for drawing with no zoom and much faster when drawing zoomed. This is great for my application as much zooming in and dragging around once zoomed is done. In addition it seems to be a more elegant solution.

I will play with for a while and hope to end up using it. Great work, thanks for posting it.

robwall
Posts: 6
Joined: Fri May 11, 2007 9:20 am
Contact:

A simple workaround for the high Jfree mem use in XYPlots

Post by robwall » Wed Nov 07, 2007 9:49 am

Thanks for the post. There are some good ideas. I have one case with ioGAS where we plot 5 or 10 million points on the screen, AND I DON'T NEED TOOLTIPS. It used to work with a two year old version of JFreeChart, but not now (out of mem ex)!

There is something fishy about the entity collection code now I think. My simple workaround is to override render in a subclass of XYPlot and pass null as the 'info' object. (and also elsewhere make sure I don't set a tooltiprenderer).

I didn't have time to merge in your changes today, but should I need further speed and mem improvements I will.

Thanks again
Rob Wall.

===example overriddedn method to pass null as info object

final public boolean render(Graphics2D g2, Rectangle2D dataArea, int index, PlotRenderingInfo info,
CrosshairState crosshairState)
{
boolean ret = super.render(g2, dataArea, index, m_wantTooltips ? info : null, crosshairState);
return ret;
}

robwall
Posts: 6
Joined: Fri May 11, 2007 9:20 am
Contact:

forgot

Post by robwall » Wed Nov 07, 2007 9:54 am

I forgot to say, this gives at least a 10 fold decrease in memory used, according to my debug outputs.

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

Re: A simple workaround for the high Jfree mem use in XYPlot

Post by david.gilbert » Wed Nov 07, 2007 11:38 am

robwall wrote:There is something fishy about the entity collection code now I think.
It's always been fishy. The tooltip for *every* item is precomputed, along with the hotspot region - it's analogous to an HTML image map. It doesn't work well for large datasets.

Most likely the renderer you used in the old version of JFreeChart didn't support tooltip generation at all, and it's been added in the interim.

Your workaround is good.
David Gilbert
JFreeChart Project Leader

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

robwall
Posts: 6
Joined: Fri May 11, 2007 9:20 am
Contact:

fishyness

Post by robwall » Wed Nov 07, 2007 3:07 pm

Dave,
Thanks for the response. That was the odd thing! The old version did somehow do tooltips and it was quicker?!?

(I think is is the ver, I copied this from JFreeChart.java
" * $Id: JFreeChart.java,v 1.18 2002/10/18 14:37:03 mungady Exp $")

I would investigate further but now I have a workaround its not worth my time, or yours I expect! Thanks for the response anyway.

You know you have done a good job when people replace a few or implement your interfaces. You know you havn't when they just throw it all away ...
Cheers
Rob

arwelbath
Posts: 24
Joined: Fri Aug 10, 2007 12:37 pm

Post by arwelbath » Wed Nov 14, 2007 10:44 am

Fat XYPlot not working with multiple series. Bug???

The snippet below plots two XYSeriesCollection on the same graph, and with a different renderer for each. But, if I replace the XYPlot with FastXYPlot, then it will only display both collections if they are plotted with the same renderer. If I try to use two different renderers, only one is drawn. Is this a bug or have I misunderstood fastXPPlot??

Cheers,
Arwel

Code: Select all

        public ChartPanel mkPointsAndLinesSemilogChart(XYSeriesCollection pointsColl, XYSeriesCollection linesColl, String name) {
        NumberAxis xAxis = new NumberAxis("Qz");
        LogarithmicAxis yAxis = new LogarithmicAxis("Ref");
        yAxis.setStrictValuesFlag(false);
        
        //Make the symbols plot....
        XYLineAndShapeRenderer rendererPoints = new XYLineAndShapeRenderer();
        rendererPoints.setShapesVisible(true);
        rendererPoints.setShapesFilled(false);
        rendererPoints.setLinesVisible(false);

        //Make the lines plot....
        XYLineAndShapeRenderer rendererLines = new XYLineAndShapeRenderer();
        rendererLines.setLinesVisible(true);
        rendererLines.setShapesVisible(false);
        
        XYPlot plot = new XYPlot(pointsColl, xAxis, yAxis, rendererPoints);
        plot.setBackgroundPaint(Color.white);
        plot.setDomainGridlinesVisible(false);
        plot.setRangeGridlinesVisible(false);
        
        plot.setDataset(1,linesColl);
        plot.setRenderer(1,rendererLines);
        
        JFreeChart chart = new JFreeChart("Reflectivity", plot);
        chart.removeLegend();
        chart.setAntiAlias(false);
        final ChartPanel panel = new ChartPanel(chart);
        ch = chart;
        return panel;
    }

Locked