spinworld.gui.RadarPlot.java Source code

Java tutorial

Introduction

Here is the source code for spinworld.gui.RadarPlot.java

Source

/* ===========================================================
 * JFreeChart : a free chart library for the Java(tm) platform
 * ===========================================================
 *
 * (C) Copyright 2000-2011, by Object Refinery Limited and Contributors.
 *
 * Project Info:  http://www.jfree.org/jfreechart/index.html
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
 * USA.
 *
 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.]
 *
 * ------------------
 * SpiderWebPlot.java
 * ------------------
 * (C) Copyright 2005-2008, by Heaps of Flavour Pty Ltd and Contributors.
 *
 * Company Info:  http://www.i4-talent.com
 *
 * Original Author:  Don Elliott;
 * Contributor(s):   David Gilbert (for Object Refinery Limited);
 *                   Nina Jeliazkova;
 *
 * Changes
 * -------
 * 28-Jan-2005 : First cut - missing a few features - still to do:
 *                           - needs tooltips/URL/label generator functions
 *                           - ticks on axes / background grid?
 * 31-Jan-2005 : Renamed SpiderWebPlot, added label generator support, and
 *               reformatted for consistency with other source files in
 *               JFreeChart (DG);
 * 20-Apr-2005 : Renamed CategoryLabelGenerator
 *               --> CategoryItemLabelGenerator (DG);
 * 05-May-2005 : Updated draw() method parameters (DG);
 * 10-Jun-2005 : Added equals() method and fixed serialization (DG);
 * 16-Jun-2005 : Added default constructor and get/setDataset()
 *               methods (DG);
 * ------------- JFREECHART 1.0.x ---------------------------------------------
 * 05-Apr-2006 : Fixed bug preventing the display of zero values - see patch
 *               1462727 (DG);
 * 05-Apr-2006 : Added support for mouse clicks, tool tips and URLs - see patch
 *               1463455 (DG);
 * 01-Jun-2006 : Fix bug 1493199, NullPointerException when drawing with null
 *               info (DG);
 * 05-Feb-2007 : Added attributes for axis stroke and paint, while fixing
 *               bug 1651277, and implemented clone() properly (DG);
 * 06-Feb-2007 : Changed getPlotValue() to protected, as suggested in bug
 *               1605202 (DG);
 * 05-Mar-2007 : Restore clip region correctly (see bug 1667750) (DG);
 * 18-May-2007 : Set dataset for LegendItem (DG);
 * 02-Jun-2008 : Fixed bug with chart entities using TableOrder.BY_COLUMN (DG);
 * 02-Jun-2008 : Fixed bug with null dataset (DG);
 * 01-Jun-2009 : Set series key in getLegendItems() (DG);
 *
 */

package spinworld.gui;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.font.FontRenderContext;
import java.awt.font.LineMetrics;
import java.awt.geom.Arc2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.jfree.chart.LegendItem;
import org.jfree.chart.LegendItemCollection;
import org.jfree.chart.entity.CategoryItemEntity;
import org.jfree.chart.entity.EntityCollection;
import org.jfree.chart.event.PlotChangeEvent;
import org.jfree.chart.labels.CategoryItemLabelGenerator;
import org.jfree.chart.labels.CategoryToolTipGenerator;
import org.jfree.chart.labels.StandardCategoryItemLabelGenerator;
import org.jfree.chart.plot.DrawingSupplier;
import org.jfree.chart.plot.Plot;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.plot.PlotState;
import org.jfree.chart.urls.CategoryURLGenerator;
import org.jfree.data.category.CategoryDataset;
import org.jfree.data.general.DatasetChangeEvent;
import org.jfree.data.general.DatasetUtilities;
import org.jfree.io.SerialUtilities;
import org.jfree.ui.RectangleInsets;
import org.jfree.util.ObjectUtilities;
import org.jfree.util.PaintList;
import org.jfree.util.PaintUtilities;
import org.jfree.util.Rotation;
import org.jfree.util.ShapeUtilities;
import org.jfree.util.StrokeList;
import org.jfree.util.TableOrder;

/**
 * A plot that displays data from a {@link CategoryDataset} in the form of a
 * "spider web".  Multiple series can be plotted on the same axis to allow
 * easy comparison.  This plot doesn't support negative values at present.
 */
public class RadarPlot extends Plot implements Cloneable, Serializable {

    /** For serialization. */
    private static final long serialVersionUID = -5376340422031599463L;

    /** The default head radius percent (currently 1%). */
    public static final double DEFAULT_HEAD = 0.01;

    /** The default axis label gap (currently 10%). */
    public static final double DEFAULT_AXIS_LABEL_GAP = 0.10;

    /** The default interior gap. */
    public static final double DEFAULT_INTERIOR_GAP = 0.25;

    /** The maximum interior gap (currently 40%). */
    public static final double MAX_INTERIOR_GAP = 0.40;

    /** The default starting angle for the radar chart axes. */
    public static final double DEFAULT_START_ANGLE = 90.0;

    /** The default series label font. */
    public static final Font DEFAULT_LABEL_FONT = new Font("SansSerif", Font.PLAIN, 10);

    /** The default series label paint. */
    public static final Paint DEFAULT_LABEL_PAINT = Color.black;

    /** The default series label background paint. */
    public static final Paint DEFAULT_LABEL_BACKGROUND_PAINT = new Color(255, 255, 192);

    /** The default series label outline paint. */
    public static final Paint DEFAULT_LABEL_OUTLINE_PAINT = Color.black;

    /** The default series label outline stroke. */
    public static final Stroke DEFAULT_LABEL_OUTLINE_STROKE = new BasicStroke(0.5f);

    /** The default series label shadow paint. */
    public static final Paint DEFAULT_LABEL_SHADOW_PAINT = Color.lightGray;

    /**
     * Length of the tick mark.
     * <P>
     * <B>Note</B>: hardcoded for now, to be customizable in the future.
     */
    private static final double TICK_MARK_LENGTH = 6d;

    /** The head radius as a percentage of the available drawing area. */
    protected double headPercent;

    /** Stroke to be used for head drawing. */
    protected Stroke headOutlineStroke;

    /** The space left around the outside of the plot as a percentage. */
    private double interiorGap;

    /** The gap between the labels and the axes as a %age of the radius. */
    private double axisLabelGap;

    /**
     * The paint used to draw the axis lines.
     *
     * @since 1.0.4
     */
    private transient Paint axisLinePaint;

    /**
     * The stroke used to draw the axis lines.
     *
     * @since 1.0.4
     */
    private transient Stroke axisLineStroke;

    /** The dataset. */
    private CategoryDataset dataset;

    /** The maximum value we are plotting against on each category axis. */
    private Double maxValue;

    /**
     * Maximum values of individual categories. Note that the field
     * <code>maxValue</code> has always higher priority. If it is set this field
     * is ignored.
     */
    private Map<Integer, Double> maxValues;

    /** The origin common for all categories axes. */
    private Double origin;

    /**
     * Origins for individual categories. Note that the field
     * <code>origin</code> has always higher priority. If it is set this field
     * is ignored.
     */
    private Map<Integer, Double> origins;

    /**
     * The data extract order (BY_ROW or BY_COLUMN). This denotes whether
     * the data series are stored in rows (in which case the category names are
     * derived from the column keys) or in columns (in which case the category
     * names are derived from the row keys).
     */
    private TableOrder dataExtractOrder;

    /** The starting angle. */
    private double startAngle;

    /** The direction for drawing the radar axis & plots. */
    private Rotation direction;

    /** The legend item shape. */
    private transient Shape legendItemShape;

    /** The paint for ALL series (overrides list). */
    private transient Paint seriesPaint;

    /** The series paint list. */
    private PaintList seriesPaintList;

    /** The base series paint (fallback). */
    private transient Paint baseSeriesPaint;

    /** The outline paint for ALL series (overrides list). */
    private transient Paint seriesOutlinePaint;

    /** The series outline paint list. */
    private PaintList seriesOutlinePaintList;

    /** The base series outline paint (fallback). */
    private transient Paint baseSeriesOutlinePaint;

    /** The outline stroke for ALL series (overrides list). */
    private transient Stroke seriesOutlineStroke;

    /** The series outline stroke list. */
    private StrokeList seriesOutlineStrokeList;

    /** The base series outline stroke (fallback). */
    private transient Stroke baseSeriesOutlineStroke;

    /** The font used to display the category labels. */
    private Font labelFont;

    /** The color used to draw the category labels. */
    private transient Paint labelPaint;

    /** The label generator. */
    private CategoryItemLabelGenerator labelGenerator;

    /** controls if the web polygons are filled or not */
    private boolean webFilled = true;

    /** A tooltip generator for the plot (<code>null</code> permitted). */
    private CategoryToolTipGenerator toolTipGenerator;

    /** A URL generator for the plot (<code>null</code> permitted). */
    private CategoryURLGenerator urlGenerator;

    /** Whether to draw tick marks and labels. */
    private boolean axisTickVisible;

    /** Whether to draw data point with values of out of axis range. */
    private boolean drawOutOfRangePoints;

    /**
     * Creates a default plot with no dataset.
     */
    public RadarPlot() {
        this(null);
    }

    /**
     * Creates a new spider web plot with the given dataset, with each row
     * representing a series.
     *
     * @param dataset  the dataset (<code>null</code> permitted).
     */
    public RadarPlot(CategoryDataset dataset) {
        this(dataset, TableOrder.BY_ROW);
    }

    /**
     * Creates a new spider web plot with the given dataset.
     *
     * @param dataset  the dataset.
     * @param extract  controls how data is extracted ({@link TableOrder#BY_ROW}
     *                 or {@link TableOrder#BY_COLUMN}).
     */
    public RadarPlot(CategoryDataset dataset, TableOrder extract) {
        super();
        if (extract == null) {
            throw new IllegalArgumentException("Null 'extract' argument.");
        }
        this.dataset = dataset;
        if (dataset != null) {
            dataset.addChangeListener(this);
        }

        this.dataExtractOrder = extract;
        this.headPercent = DEFAULT_HEAD;
        this.axisLabelGap = DEFAULT_AXIS_LABEL_GAP;
        this.axisLinePaint = Color.black;
        this.axisLineStroke = new BasicStroke(1.0f);

        this.interiorGap = DEFAULT_INTERIOR_GAP;
        this.startAngle = DEFAULT_START_ANGLE;
        this.direction = Rotation.CLOCKWISE;

        this.seriesPaint = null;
        this.seriesPaintList = new PaintList();
        this.baseSeriesPaint = null;

        this.seriesOutlinePaint = null;
        this.seriesOutlinePaintList = new PaintList();
        this.baseSeriesOutlinePaint = DEFAULT_OUTLINE_PAINT;

        this.seriesOutlineStroke = null;
        this.seriesOutlineStrokeList = new StrokeList();
        this.baseSeriesOutlineStroke = DEFAULT_OUTLINE_STROKE;

        this.labelFont = DEFAULT_LABEL_FONT;
        this.labelPaint = DEFAULT_LABEL_PAINT;
        this.labelGenerator = new StandardCategoryItemLabelGenerator();

        this.legendItemShape = DEFAULT_LEGEND_ITEM_CIRCLE;

        this.origins = new HashMap<Integer, Double>();
        this.maxValues = new HashMap<Integer, Double>();
    }

    /**
     * Returns a short string describing the type of plot.
     *
     * @return The plot type.
     */
    public String getPlotType() {
        // return localizationResources.getString("Radar_Plot");
        return ("Spider Web Plot");
    }

    /**
     * Returns the dataset.
     *
     * @return The dataset (possibly <code>null</code>).
     *
     * @see #setDataset(CategoryDataset)
     */
    public CategoryDataset getDataset() {
        return this.dataset;
    }

    /**
     * Sets the dataset used by the plot and sends a {@link PlotChangeEvent}
     * to all registered listeners.
     *
     * @param dataset  the dataset (<code>null</code> permitted).
     *
     * @see #getDataset()
     */
    public void setDataset(CategoryDataset dataset) {
        // if there is an existing dataset, remove the plot from the list of
        // change listeners...
        if (this.dataset != null) {
            this.dataset.removeChangeListener(this);
        }

        // set the new dataset, and register the chart as a change listener...
        this.dataset = dataset;
        if (dataset != null) {
            setDatasetGroup(dataset.getGroup());
            dataset.addChangeListener(this);
        }

        resetBoundaryValues();

        // send a dataset change event to self to trigger plot change event
        datasetChanged(new DatasetChangeEvent(this, dataset));
    }

    private void resetBoundaryValues() {
        this.maxValues.clear();
        this.origins.clear();
    }

    /**
     * Method to determine if the web chart is to be filled.
     *
     * @return A boolean.
     *
     * @see #setWebFilled(boolean)
     */
    public boolean isWebFilled() {
        return this.webFilled;
    }

    /**
     * Sets the webFilled flag and sends a {@link PlotChangeEvent} to all
     * registered listeners.
     *
     * @param flag  the flag.
     *
     * @see #isWebFilled()
     */
    public void setWebFilled(boolean flag) {
        this.webFilled = flag;
        fireChangeEvent();
    }

    /**
     * Returns the data extract order (by row or by column).
     *
     * @return The data extract order (never <code>null</code>).
     *
     * @see #setDataExtractOrder(TableOrder)
     */
    public TableOrder getDataExtractOrder() {
        return this.dataExtractOrder;
    }

    /**
     * Sets the data extract order (by row or by column) and sends a
     * {@link PlotChangeEvent}to all registered listeners.
     *
     * @param order the order (<code>null</code> not permitted).
     *
     * @throws IllegalArgumentException if <code>order</code> is
     *     <code>null</code>.
     *
     * @see #getDataExtractOrder()
     */
    public void setDataExtractOrder(TableOrder order) {
        if (order == null) {
            throw new IllegalArgumentException("Null 'order' argument");
        }
        this.dataExtractOrder = order;
        fireChangeEvent();
    }

    /**
     * Returns the head percent.
     *
     * @return The head percent.
     *
     * @see #setHeadPercent(double)
     */
    public double getHeadPercent() {
        return this.headPercent;
    }

    /**
     * Sets the head percent and sends a {@link PlotChangeEvent} to all
     * registered listeners.
     *
     * @param percent  the percent.
     *
     * @see #getHeadPercent()
     */
    public void setHeadPercent(double percent) {
        this.headPercent = percent;
        fireChangeEvent();
    }

    /**
     * Sets the head outline stroke for all series. When <code>null</code>
     * {@link #getSeriesOutlineStroke(int)} is used. Sends a {@link
     * PlotChangeEvent} to all registered listeners.
     *
     * @param headOutlineStroke head outline stroke
     */
    public void setHeadOutlineStroke(Stroke headOutlineStroke) {
        this.headOutlineStroke = headOutlineStroke;
        fireChangeEvent();
    }

    /**
     * Returns the head outline stroke.
     *
     * @return head outline stroke.
     */
    public Stroke getHeadOutlineStroke() {
        return headOutlineStroke;
    }

    private Stroke getHeadOutlineStroke(int series) {
        return headOutlineStroke == null ? getSeriesOutlineStroke(series) : headOutlineStroke;
    }

    /**
     * Returns the start angle for the first radar axis.
     * <BR>
     * This is measured in degrees starting from 3 o'clock (Java Arc2D default)
     * and measuring anti-clockwise.
     *
     * @return The start angle.
     *
     * @see #setStartAngle(double)
     */
    public double getStartAngle() {
        return this.startAngle;
    }

    /**
     * Sets the starting angle and sends a {@link PlotChangeEvent} to all
     * registered listeners.
     * <P>
     * The initial default value is 90 degrees, which corresponds to 12 o'clock.
     * A value of zero corresponds to 3 o'clock... this is the encoding used by
     * Java's Arc2D class.
     *
     * @param angle  the angle (in degrees).
     *
     * @see #getStartAngle()
     */
    public void setStartAngle(double angle) {
        this.startAngle = angle;
        fireChangeEvent();
    }

    /**
     * Returns the maximum value which is used for all categories.
     *
     * @return The maximum value.
     *
     * @see #setMaxValue(Double)
     */
    public Double getMaxValue() {
        return this.maxValue;
    }

    /**
     * Returns maximum value for a particular category.
     *
     * @param cat the category of interest
     * @return The maximum value.
     *
     * @see #setMaxValue(Double)
     */
    public Double getMaxValue(int cat) {
        return maxValue == null ? (Double) maxValues.get(new Integer(cat)) : maxValue;
    }

    /**
     * Sets the maxValue for <em>all</em> series in the plot. If this is set to
     * <code>null</code>, then a list of maxValues is used instead (to allow
     * different maxValues to be used for each series).
     *
     * @param maxValue the maxValue (<code>null</code> permitted).
     */
    public void setMaxValue(Double maxValue) {
        this.maxValue = maxValue;
        fireChangeEvent();
    }

    /**
     * Sets maximum value for category.
     *
     * @param cat the category of interest
     * @param maxValue the maximum value for the category
     */
    public void setMaxValue(int cat, Double maxValue) {
        maxValues.put(new Integer(cat), maxValue);
        fireChangeEvent();
    }

    public Double getOrigin(int cat) {
        return origin == null ? (Double) origins.get(new Integer(cat)) : origin;
    }

    /**
     * Sets origin for category.
     *
     * @param cat the category of interest
     * @param origin the origin for the category
     */
    public void setOrigin(int cat, Double origin) {
        origins.put(new Integer(cat), origin);
        fireChangeEvent();
    }

    /**
     * Sets the origin for <em>all</em> series in the plot. If this is set to
     * <code>null</code>, then a list of origins is used instead (to allow
     * different origins to be used for each series).
     *
     * @param origin the origin (<code>null</code> permitted).
     */
    public void setOrigin(Double origin) {
        this.origin = origin;
        fireChangeEvent();
    }

    /**
     * Returns the direction in which the radar axes are drawn
     * (clockwise or anti-clockwise).
     *
     * @return The direction (never <code>null</code>).
     *
     * @see #setDirection(Rotation)
     */
    public Rotation getDirection() {
        return this.direction;
    }

    /**
     * Sets the direction in which the radar axes are drawn and sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param direction  the direction (<code>null</code> not permitted).
     *
     * @see #getDirection()
     */
    public void setDirection(Rotation direction) {
        if (direction == null) {
            throw new IllegalArgumentException("Null 'direction' argument.");
        }
        this.direction = direction;
        fireChangeEvent();
    }

    /**
     * Returns the interior gap, measured as a percentage of the available
     * drawing space.
     *
     * @return The gap (as a percentage of the available drawing space).
     *
     * @see #setInteriorGap(double)
     */
    public double getInteriorGap() {
        return this.interiorGap;
    }

    /**
     * Sets the interior gap and sends a {@link PlotChangeEvent} to all
     * registered listeners. This controls the space between the edges of the
     * plot and the plot area itself (the region where the axis labels appear).
     *
     * @param percent  the gap (as a percentage of the available drawing space).
     *
     * @see #getInteriorGap()
     */
    public void setInteriorGap(double percent) {
        if ((percent < 0.0) || (percent > MAX_INTERIOR_GAP)) {
            throw new IllegalArgumentException("Percentage outside valid range.");
        }
        if (this.interiorGap != percent) {
            this.interiorGap = percent;
            fireChangeEvent();
        }
    }

    /**
     * Returns the axis label gap.
     *
     * @return The axis label gap.
     *
     * @see #setAxisLabelGap(double)
     */
    public double getAxisLabelGap() {
        return this.axisLabelGap;
    }

    /**
     * Sets the axis label gap and sends a {@link PlotChangeEvent} to all
     * registered listeners.
     *
     * @param gap  the gap.
     *
     * @see #getAxisLabelGap()
     */
    public void setAxisLabelGap(double gap) {
        this.axisLabelGap = gap;
        fireChangeEvent();
    }

    /**
     * Returns the paint used to draw the axis lines.
     *
     * @return The paint used to draw the axis lines (never <code>null</code>).
     *
     * @see #setAxisLinePaint(Paint)
     * @see #getAxisLineStroke()
     * @since 1.0.4
     */
    public Paint getAxisLinePaint() {
        return this.axisLinePaint;
    }

    /**
     * Sets the paint used to draw the axis lines and sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param paint  the paint (<code>null</code> not permitted).
     *
     * @see #getAxisLinePaint()
     * @since 1.0.4
     */
    public void setAxisLinePaint(Paint paint) {
        if (paint == null) {
            throw new IllegalArgumentException("Null 'paint' argument.");
        }
        this.axisLinePaint = paint;
        fireChangeEvent();
    }

    /**
     * Returns the stroke used to draw the axis lines.
     *
     * @return The stroke used to draw the axis lines (never <code>null</code>).
     *
     * @see #setAxisLineStroke(Stroke)
     * @see #getAxisLinePaint()
     * @since 1.0.4
     */
    public Stroke getAxisLineStroke() {
        return this.axisLineStroke;
    }

    /**
     * Sets the stroke used to draw the axis lines and sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param stroke  the stroke (<code>null</code> not permitted).
     *
     * @see #getAxisLineStroke()
     * @since 1.0.4
     */
    public void setAxisLineStroke(Stroke stroke) {
        if (stroke == null) {
            throw new IllegalArgumentException("Null 'stroke' argument.");
        }
        this.axisLineStroke = stroke;
        fireChangeEvent();
    }

    //// SERIES PAINT /////////////////////////

    /**
     * Returns the paint for ALL series in the plot.
     *
     * @return The paint (possibly <code>null</code>).
     *
     * @see #setSeriesPaint(Paint)
     */
    public Paint getSeriesPaint() {
        return this.seriesPaint;
    }

    /**
     * Sets the paint for ALL series in the plot. If this is set to</code> null
     * </code>, then a list of paints is used instead (to allow different colors
     * to be used for each series of the radar group).
     *
     * @param paint the paint (<code>null</code> permitted).
     *
     * @see #getSeriesPaint()
     */
    public void setSeriesPaint(Paint paint) {
        this.seriesPaint = paint;
        fireChangeEvent();
    }

    /**
     * Returns the paint for the specified series.
     *
     * @param series  the series index (zero-based).
     *
     * @return The paint (never <code>null</code>).
     *
     * @see #setSeriesPaint(int, Paint)
     */
    public Paint getSeriesPaint(int series) {

        // return the override, if there is one...
        if (this.seriesPaint != null) {
            return this.seriesPaint;
        }

        // otherwise look up the paint list
        Paint result = this.seriesPaintList.getPaint(series);
        if (result == null) {
            DrawingSupplier supplier = getDrawingSupplier();
            if (supplier != null) {
                Paint p = supplier.getNextPaint();
                this.seriesPaintList.setPaint(series, p);
                result = p;
            } else {
                result = this.baseSeriesPaint;
            }
        }
        return result;

    }

    /**
     * Sets the paint used to fill a series of the radar and sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param series  the series index (zero-based).
     * @param paint  the paint (<code>null</code> permitted).
     *
     * @see #getSeriesPaint(int)
     */
    public void setSeriesPaint(int series, Paint paint) {
        this.seriesPaintList.setPaint(series, paint);
        fireChangeEvent();
    }

    /**
     * Returns the base series paint. This is used when no other paint is
     * available.
     *
     * @return The paint (never <code>null</code>).
     *
     * @see #setBaseSeriesPaint(Paint)
     */
    public Paint getBaseSeriesPaint() {
        return this.baseSeriesPaint;
    }

    /**
     * Sets the base series paint.
     *
     * @param paint  the paint (<code>null</code> not permitted).
     *
     * @see #getBaseSeriesPaint()
     */
    public void setBaseSeriesPaint(Paint paint) {
        if (paint == null) {
            throw new IllegalArgumentException("Null 'paint' argument.");
        }
        this.baseSeriesPaint = paint;
        fireChangeEvent();
    }

    //// SERIES OUTLINE PAINT ////////////////////////////

    /**
     * Returns the outline paint for ALL series in the plot.
     *
     * @return The paint (possibly <code>null</code>).
     */
    public Paint getSeriesOutlinePaint() {
        return this.seriesOutlinePaint;
    }

    /**
     * Sets the outline paint for ALL series in the plot. If this is set to
     * </code> null</code>, then a list of paints is used instead (to allow
     * different colors to be used for each series).
     *
     * @param paint  the paint (<code>null</code> permitted).
     */
    public void setSeriesOutlinePaint(Paint paint) {
        this.seriesOutlinePaint = paint;
        fireChangeEvent();
    }

    /**
     * Returns the paint for the specified series.
     *
     * @param series  the series index (zero-based).
     *
     * @return The paint (never <code>null</code>).
     */
    public Paint getSeriesOutlinePaint(int series) {
        // return the override, if there is one...
        if (this.seriesOutlinePaint != null) {
            return this.seriesOutlinePaint;
        }
        // otherwise look up the paint list
        Paint result = this.seriesOutlinePaintList.getPaint(series);
        if (result == null) {
            result = this.baseSeriesOutlinePaint;
        }
        return result;
    }

    /**
     * Sets the paint used to fill a series of the radar and sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param series  the series index (zero-based).
     * @param paint  the paint (<code>null</code> permitted).
     */
    public void setSeriesOutlinePaint(int series, Paint paint) {
        this.seriesOutlinePaintList.setPaint(series, paint);
        fireChangeEvent();
    }

    /**
     * Returns the base series paint. This is used when no other paint is
     * available.
     *
     * @return The paint (never <code>null</code>).
     */
    public Paint getBaseSeriesOutlinePaint() {
        return this.baseSeriesOutlinePaint;
    }

    /**
     * Sets the base series paint.
     *
     * @param paint  the paint (<code>null</code> not permitted).
     */
    public void setBaseSeriesOutlinePaint(Paint paint) {
        if (paint == null) {
            throw new IllegalArgumentException("Null 'paint' argument.");
        }
        this.baseSeriesOutlinePaint = paint;
        fireChangeEvent();
    }

    //// SERIES OUTLINE STROKE /////////////////////

    /**
     * Returns the outline stroke for ALL series in the plot.
     *
     * @return The stroke (possibly <code>null</code>).
     */
    public Stroke getSeriesOutlineStroke() {
        return this.seriesOutlineStroke;
    }

    /**
     * Sets the outline stroke for ALL series in the plot. If this is set to
     * </code> null</code>, then a list of paints is used instead (to allow
     * different colors to be used for each series).
     *
     * @param stroke  the stroke (<code>null</code> permitted).
     */
    public void setSeriesOutlineStroke(Stroke stroke) {
        this.seriesOutlineStroke = stroke;
        fireChangeEvent();
    }

    /**
     * Returns the stroke for the specified series.
     *
     * @param series  the series index (zero-based).
     *
     * @return The stroke (never <code>null</code>).
     */
    public Stroke getSeriesOutlineStroke(int series) {

        // return the override, if there is one...
        if (this.seriesOutlineStroke != null) {
            return this.seriesOutlineStroke;
        }

        // otherwise look up the paint list
        Stroke result = this.seriesOutlineStrokeList.getStroke(series);
        if (result == null) {
            result = this.baseSeriesOutlineStroke;
        }
        return result;

    }

    /**
     * Sets the stroke used to fill a series of the radar and sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param series  the series index (zero-based).
     * @param stroke  the stroke (<code>null</code> permitted).
     */
    public void setSeriesOutlineStroke(int series, Stroke stroke) {
        this.seriesOutlineStrokeList.setStroke(series, stroke);
        fireChangeEvent();
    }

    /**
     * Returns the base series stroke. This is used when no other stroke is
     * available.
     *
     * @return The stroke (never <code>null</code>).
     */
    public Stroke getBaseSeriesOutlineStroke() {
        return this.baseSeriesOutlineStroke;
    }

    /**
     * Sets the base series stroke.
     *
     * @param stroke  the stroke (<code>null</code> not permitted).
     */
    public void setBaseSeriesOutlineStroke(Stroke stroke) {
        if (stroke == null) {
            throw new IllegalArgumentException("Null 'stroke' argument.");
        }
        this.baseSeriesOutlineStroke = stroke;
        fireChangeEvent();
    }

    /**
     * Returns the shape used for legend items.
     *
     * @return The shape (never <code>null</code>).
     *
     * @see #setLegendItemShape(Shape)
     */
    public Shape getLegendItemShape() {
        return this.legendItemShape;
    }

    /**
     * Sets the shape used for legend items and sends a {@link PlotChangeEvent}
     * to all registered listeners.
     *
     * @param shape  the shape (<code>null</code> not permitted).
     *
     * @see #getLegendItemShape()
     */
    public void setLegendItemShape(Shape shape) {
        if (shape == null) {
            throw new IllegalArgumentException("Null 'shape' argument.");
        }
        this.legendItemShape = shape;
        fireChangeEvent();
    }

    /**
     * Returns the series label font.
     *
     * @return The font (never <code>null</code>).
     *
     * @see #setLabelFont(Font)
     */
    public Font getLabelFont() {
        return this.labelFont;
    }

    /**
     * Sets the series label font and sends a {@link PlotChangeEvent} to all
     * registered listeners.
     *
     * @param font  the font (<code>null</code> not permitted).
     *
     * @see #getLabelFont()
     */
    public void setLabelFont(Font font) {
        if (font == null) {
            throw new IllegalArgumentException("Null 'font' argument.");
        }
        this.labelFont = font;
        fireChangeEvent();
    }

    /**
     * Returns the series label paint.
     *
     * @return The paint (never <code>null</code>).
     *
     * @see #setLabelPaint(Paint)
     */
    public Paint getLabelPaint() {
        return this.labelPaint;
    }

    /**
     * Sets the series label paint and sends a {@link PlotChangeEvent} to all
     * registered listeners.
     *
     * @param paint  the paint (<code>null</code> not permitted).
     *
     * @see #getLabelPaint()
     */
    public void setLabelPaint(Paint paint) {
        if (paint == null) {
            throw new IllegalArgumentException("Null 'paint' argument.");
        }
        this.labelPaint = paint;
        fireChangeEvent();
    }

    /**
     * Returns the label generator.
     *
     * @return The label generator (never <code>null</code>).
     *
     * @see #setLabelGenerator(CategoryItemLabelGenerator)
     */
    public CategoryItemLabelGenerator getLabelGenerator() {
        return this.labelGenerator;
    }

    /**
     * Sets the label generator and sends a {@link PlotChangeEvent} to all
     * registered listeners.
     *
     * @param generator  the generator (<code>null</code> not permitted).
     *
     * @see #getLabelGenerator()
     */
    public void setLabelGenerator(CategoryItemLabelGenerator generator) {
        if (generator == null) {
            throw new IllegalArgumentException("Null 'generator' argument.");
        }
        this.labelGenerator = generator;
    }

    /**
     * Returns the tool tip generator for the plot.
     *
     * @return The tool tip generator (possibly <code>null</code>).
     *
     * @see #setToolTipGenerator(CategoryToolTipGenerator)
     *
     * @since 1.0.2
     */
    public CategoryToolTipGenerator getToolTipGenerator() {
        return this.toolTipGenerator;
    }

    /**
     * Sets the tool tip generator for the plot and sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param generator  the generator (<code>null</code> permitted).
     *
     * @see #getToolTipGenerator()
     *
     * @since 1.0.2
     */
    public void setToolTipGenerator(CategoryToolTipGenerator generator) {
        this.toolTipGenerator = generator;
        fireChangeEvent();
    }

    /**
     * Returns the URL generator for the plot.
     *
     * @return The URL generator (possibly <code>null</code>).
     *
     * @see #setURLGenerator(CategoryURLGenerator)
     *
     * @since 1.0.2
     */
    public CategoryURLGenerator getURLGenerator() {
        return this.urlGenerator;
    }

    /**
     * Sets the URL generator for the plot and sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param generator  the generator (<code>null</code> permitted).
     *
     * @see #getURLGenerator()
     *
     * @since 1.0.2
     */
    public void setURLGenerator(CategoryURLGenerator generator) {
        this.urlGenerator = generator;
        fireChangeEvent();
    }

    /**
     * Whether tick marks and labels are visible.
     *
     * @return the visibility of tick marks
     */
    public boolean isAxisTickVisible() {
        return axisTickVisible;
    }

    /**
     * Set whether tick marks and labels shall be visible.
     *
     * @param visible the visibility of tick marks
     */
    public void setAxisTickVisible(boolean visible) {
        this.axisTickVisible = visible;
        fireChangeEvent();
    }

    /**
     * Whether data points with values of out of axis range are drawn.
     *
     * @return are data points with values of out of axis drawn?
     */
    public boolean isDrawOutOfRangePoints() {
        return drawOutOfRangePoints;
    }

    /**
     * Set whether data points with values of out of axis range shall be drawn.
     *
     * @param drawOutOfRangePoints shall be the points with values of out of
     *        axis range drawn
     */
    public void setDrawOutOfRangePoints(boolean drawOutOfRangePoints) {
        this.drawOutOfRangePoints = drawOutOfRangePoints;
        fireChangeEvent();
    }

    /**
     * Returns a collection of legend items for the spider web chart.
     *
     * @return The legend items (never <code>null</code>).
     */
    public LegendItemCollection getLegendItems() {
        LegendItemCollection result = new LegendItemCollection();
        if (getDataset() == null) {
            return result;
        }
        List<?> keys = null;
        if (this.dataExtractOrder == TableOrder.BY_ROW) {
            keys = this.dataset.getRowKeys();
        } else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
            keys = this.dataset.getColumnKeys();
        }
        if (keys == null) {
            return result;
        }

        int series = 0;
        Iterator<?> iterator = keys.iterator();
        Shape shape = getLegendItemShape();
        while (iterator.hasNext()) {
            Comparable<?> key = (Comparable<?>) iterator.next();
            String label = key.toString();
            String description = label;
            Paint paint = getSeriesPaint(series);
            Paint outlinePaint = getSeriesOutlinePaint(series);
            Stroke stroke = getHeadOutlineStroke(series);
            LegendItem item = new LegendItem(label, description, null, null, shape, paint, stroke, outlinePaint);
            item.setDataset(getDataset());
            item.setSeriesKey(key);
            item.setSeriesIndex(series);
            result.add(item);
            series++;
        }
        return result;
    }

    /**
     * Returns a cartesian point from a polar angle, length and bounding box
     *
     * @param bounds  the area inside which the point needs to be.
     * @param angle  the polar angle, in degrees.
     * @param length  the relative length. Given in percent of maximum extend.
     *
     * @return The cartesian point.
     */
    protected Point2D getWebPoint(Rectangle2D bounds, double angle, double length) {

        double angrad = Math.toRadians(angle);
        double x = Math.cos(angrad) * length * bounds.getWidth() / 2;
        double y = -Math.sin(angrad) * length * bounds.getHeight() / 2;

        return new Point2D.Double(bounds.getX() + x + bounds.getWidth() / 2,
                bounds.getY() + y + bounds.getHeight() / 2);
    }

    /**
     * Draws the plot on a Java 2D graphics device (such as the screen or a
     * printer).
     *
     * @param g2  the graphics device.
     * @param area  the area within which the plot should be drawn.
     * @param anchor  the anchor point (<code>null</code> permitted).
     * @param parentState  the state from the parent plot, if there is one.
     * @param info  collects info about the drawing.
     */
    public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor, PlotState parentState,
            PlotRenderingInfo info) {

        // adjust for insets...
        RectangleInsets insets = getInsets();
        insets.trim(area);

        if (info != null) {
            info.setPlotArea(area);
            info.setDataArea(area);
        }

        drawBackground(g2, area);
        drawOutline(g2, area);

        Shape savedClip = g2.getClip();

        g2.clip(area);
        Composite originalComposite = g2.getComposite();
        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, getForegroundAlpha()));

        if (!DatasetUtilities.isEmptyOrNull(this.dataset)) {
            int seriesCount = 0, catCount = 0;

            if (this.dataExtractOrder == TableOrder.BY_ROW) {
                seriesCount = this.dataset.getRowCount();
                catCount = this.dataset.getColumnCount();
            } else {
                seriesCount = this.dataset.getColumnCount();
                catCount = this.dataset.getRowCount();
            }

            // ensure we have origin and maximum value for each axis
            ensureBoundaryValues(seriesCount, catCount);

            // Next, setup the plot area

            // adjust the plot area by the interior spacing value

            double gapHorizontal = area.getWidth() * getInteriorGap();
            double gapVertical = area.getHeight() * getInteriorGap();

            double X = area.getX() + gapHorizontal / 2;
            double Y = area.getY() + gapVertical / 2;
            double W = area.getWidth() - gapHorizontal;
            double H = area.getHeight() - gapVertical;

            double headW = area.getWidth() * this.headPercent;
            double headH = area.getHeight() * this.headPercent;

            // make the chart area a square
            double min = Math.min(W, H) / 2;
            X = (X + X + W) / 2 - min;
            Y = (Y + Y + H) / 2 - min;
            W = 2 * min;
            H = 2 * min;

            Point2D centre = new Point2D.Double(X + W / 2, Y + H / 2);
            Rectangle2D radarArea = new Rectangle2D.Double(X, Y, W, H);

            // draw the axis and category label
            for (int cat = 0; cat < catCount; cat++) {
                double angle = getStartAngle() + (getDirection().getFactor() * cat * 360 / catCount);

                Point2D endPoint = getWebPoint(radarArea, angle, 1);
                // 1 = end of axis
                Line2D line = new Line2D.Double(centre, endPoint);
                g2.setPaint(this.axisLinePaint);
                g2.setStroke(this.axisLineStroke);
                g2.draw(line);
                if (isAxisTickVisible()) {
                    drawTicks(g2, radarArea, angle, cat);
                }
                drawLabel(g2, area, radarArea, 0.0, cat, angle, 360.0 / catCount);
            }

            // Now actually plot each of the series polygons..
            for (int series = 0; series < seriesCount; series++) {
                drawRadarPoly(g2, radarArea, centre, info, series, catCount, headH, headW);
            }
        } else {
            drawNoDataMessage(g2, area);
        }
        g2.setClip(savedClip);
        g2.setComposite(originalComposite);
        drawOutline(g2, area);
    }

    private void drawTicks(Graphics2D g2, Rectangle2D radarArea, double axisAngle, int cat) {
        double[] ticks = { 0.5d, 1d };
        for (int i = 0; i < ticks.length; i++) {
            double tick = ticks[i];
            Point2D middlePoint = getWebPoint(radarArea, axisAngle, tick);
            double xt = middlePoint.getX();
            double yt = middlePoint.getY();
            double angrad = Math.toRadians(-axisAngle);
            g2.translate(xt, yt);
            g2.rotate(angrad);
            g2.drawLine(0, (int) -TICK_MARK_LENGTH / 2, 0, (int) TICK_MARK_LENGTH / 2);
            g2.rotate(-angrad);
            g2.translate(-xt, -yt);
            drawTickLabel(g2, radarArea, middlePoint, axisAngle, cat, tick);
        }
    }

    private void drawTickLabel(Graphics2D g2, Rectangle2D radarArea, Point2D middlePoint, double _axisAngle,
            int cat, double tick) {
        double axisAngle = normalize(_axisAngle);
        double _origin = getOrigin(cat).doubleValue();
        double max = getMaxValue(cat).doubleValue();
        double tickValue = ((max - _origin) * tick) + _origin;
        String label = "" + Math.round(tickValue * 1000) / 1000d;
        FontRenderContext frc = g2.getFontRenderContext();
        Rectangle2D labelBounds = getLabelFont().getStringBounds(label, frc);
        int labelW = (int) labelBounds.getWidth();
        int labelH = (int) labelBounds.getHeight();

        double centerX = radarArea.getCenterX();
        double centerY = radarArea.getCenterY();

        double adj = middlePoint.distance(centerX, centerY);
        double opp = TICK_MARK_LENGTH / 2 + 4;
        double hyp = Math.sqrt(Math.pow(opp, 2) + Math.pow(adj, 2));
        double angle = Math.toDegrees(Math.atan(opp / adj));
        int charHeight = g2.getFontMetrics().getHeight();
        int charWidth = g2.getFontMetrics().charWidth('M');

        double alphaRad = Math.toRadians(axisAngle - angle);
        double labelX = centerX + (hyp * Math.cos(alphaRad));
        double labelY = centerY - (hyp * Math.sin(alphaRad)) + labelH;

        //        g2.draw(new Line2D.Double(centerX, centerY, labelX, labelY - labelH)); // test line

        double sinGap = Math.pow(Math.sin(Math.toRadians(axisAngle)), 2);
        if (axisAngle > 90 && axisAngle < 270) {
            labelY -= labelH;
            labelY += (charHeight * sinGap / 2);
        } else {
            labelY -= (charHeight * sinGap / 2);
        }
        double cosGap = Math.pow(Math.cos(Math.toRadians(axisAngle)), 2);
        if (axisAngle > 180) {
            labelX -= labelW;
            labelX += (charWidth * cosGap / 2);
        } else {
            labelX -= (charWidth * cosGap / 2);
        }
        //        g2.drawRect((int) labelX, (int) labelY - labelH, labelW, labelH); // test rectangle
        g2.setPaint(getLabelPaint());
        g2.setFont(getLabelFont());
        g2.drawString(label, (float) labelX, (float) labelY);
    }

    /**
     * Loop through each of the series to get the maximum value
     * on each category axis.
     *
     * @param seriesCount  the number of series
     * @param catCount  the number of categories
     */
    private void ensureBoundaryValues(int seriesCount, int catCount) {
        // base origin and maxVaue are used
        if (origin != null && maxValue != null) {
            return;
        }
        for (int catIndex = 0; catIndex < catCount; catIndex++) {
            Double preferredCatMax = getMaxValue(catIndex);
            Double preferredCatOrigin = getOrigin(catIndex);
            if (preferredCatOrigin != null && preferredCatMax != null) {
                continue; // already set per category
            }
            double catDataMaxVal = -Double.MAX_VALUE;
            double catDataMinVal = Double.MAX_VALUE;
            boolean hasValues = false;
            for (int seriesIndex = 0; seriesIndex < seriesCount; seriesIndex++) {
                Number nV = getPlotValue(seriesIndex, catIndex);
                if (nV != null) {
                    hasValues = true;
                    double v = nV.doubleValue();
                    if (v > catDataMaxVal) {
                        catDataMaxVal = v;
                    }
                    if (v < catDataMinVal) {
                        catDataMinVal = v;
                    }
                }
            }
            if (!hasValues) {
                setMaxValue(catIndex, new Double(0));
                setOrigin(catIndex, new Double(0));
                continue;
            }

            if (preferredCatMax == null) {
                preferredCatMax = new Double(catDataMaxVal);
                setMaxValue(catIndex, preferredCatMax);
            }

            // Ensure that origin of a spoke does not coincide with a data point with minimum value.
            // Such data point would lost information to which spoke (axis) it belongs since it
            // would be in the center of the spider plot, i.e. it would "belong" to all spokes.
            if (preferredCatOrigin == null) {
                double catOriginShift;
                if (catDataMaxVal == catDataMinVal) {
                    if (catDataMinVal == 0) {
                        // all data points at zero
                        catOriginShift = 0.1;
                    } else {
                        // all data points at the same value
                        catOriginShift = Math.abs(catDataMinVal / 10);
                    }
                } else {
                    // Shift origin about 10% of the data range from minimum.
                    catOriginShift = Math.abs((catDataMaxVal - catDataMinVal) / 10);
                }
                preferredCatOrigin = new Double(catDataMinVal - catOriginShift);
                setOrigin(catIndex, preferredCatOrigin);
            }

            double prefOrigin = preferredCatOrigin.doubleValue();
            double prefMax = preferredCatMax.doubleValue();
            // special case. Min, max and all values are zero.
            if (prefOrigin == 0 && prefMax == 0) {
                prefMax = 0.1;
                setMaxValue(catIndex, new Double(prefMax));
            }
            if (prefOrigin >= prefMax) {
                setMaxValue(catIndex, new Double(prefOrigin + Math.abs(prefOrigin / 2)));
            }
        }
    }

    /**
     * Draws a radar plot polygon.
     *
     * @param g2 the graphics device.
     * @param plotArea the area we are plotting in (already adjusted).
     * @param centre the centre point of the radar axes
     * @param info chart rendering info.
     * @param series the series within the dataset we are plotting
     * @param catCount the number of categories per radar plot
     * @param headH the data point height
     * @param headW the data point width
     */
    protected void drawRadarPoly(Graphics2D g2, Rectangle2D plotArea, Point2D centre, PlotRenderingInfo info,
            int series, int catCount, double headH, double headW) {

        Polygon polygon = new Polygon();

        EntityCollection entities = null;
        if (info != null) {
            entities = info.getOwner().getEntityCollection();
        }

        // plot the data...
        for (int cat = 0; cat < catCount; cat++) {

            Number dataValue = getPlotValue(series, cat);

            if (dataValue != null) {
                double value = dataValue.doubleValue();

                // Finds our starting angle from the centre for this axis

                double angle = getStartAngle() + (getDirection().getFactor() * cat * 360 / catCount);

                // The following angle calc will ensure there isn't a top
                // vertical axis - this may be useful if you don't want any
                // given criteria to 'appear' move important than the
                // others..
                //  + (getDirection().getFactor()
                //        * (cat + 0.5) * 360 / catCount);

                // find the point at the appropriate distance end point
                // along the axis/angle identified above and add it to the
                // polygon

                double _maxValue = getMaxValue(cat).doubleValue();
                double _origin = getOrigin(cat).doubleValue();
                double lowerBound = Math.min(_origin, _maxValue);
                double upperBound = Math.max(_origin, _maxValue);
                boolean lesser = value < lowerBound;
                boolean greater = value > upperBound;
                if ((lesser || greater) && !drawOutOfRangePoints) {
                    continue;
                }
                if (lesser) {
                    value = lowerBound;
                }
                if (greater) {
                    value = upperBound;
                }
                double length = _maxValue == _origin ? 0 : (value - lowerBound) / (upperBound - lowerBound);
                if (_maxValue < _origin) { // inversed
                    length = 1 - length;
                }
                Point2D point = getWebPoint(plotArea, angle, length);
                polygon.addPoint((int) point.getX(), (int) point.getY());

                Paint paint = getSeriesPaint(series);
                Paint outlinePaint = getSeriesOutlinePaint(series);

                double px = point.getX();
                double py = point.getY();
                g2.setPaint(paint);
                if (lesser || greater) {
                    // user crosshair for out-of-range data points distinguish
                    g2.setStroke(new BasicStroke(1.5f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL));
                    double delta = 3;
                    g2.draw(new Line2D.Double(px - delta, py, px + delta, py));
                    g2.draw(new Line2D.Double(px, py - delta, px, py + delta));
                } else {
                    // put an elipse at the point being plotted..
                    Ellipse2D head = new Ellipse2D.Double(px - headW / 2, py - headH / 2, headW, headH);
                    g2.fill(head);
                    g2.setStroke(getHeadOutlineStroke(series));
                    g2.setPaint(outlinePaint);
                    g2.draw(head);
                }

                if (entities != null) {
                    int row = 0;
                    int col = 0;
                    if (this.dataExtractOrder == TableOrder.BY_ROW) {
                        row = series;
                        col = cat;
                    } else {
                        row = cat;
                        col = series;
                    }
                    String tip = null;
                    if (this.toolTipGenerator != null) {
                        tip = this.toolTipGenerator.generateToolTip(this.dataset, row, col);
                    }

                    String url = null;
                    if (this.urlGenerator != null) {
                        url = this.urlGenerator.generateURL(this.dataset, row, col);
                    }

                    Shape area = new Rectangle((int) (point.getX() - headW), (int) (point.getY() - headH),
                            (int) (headW * 2), (int) (headH * 2));
                    CategoryItemEntity entity = new CategoryItemEntity(area, tip, url, this.dataset,
                            this.dataset.getRowKey(row), this.dataset.getColumnKey(col));
                    entities.add(entity);
                }
            }
        }
        // Plot the polygon

        Paint paint = getSeriesPaint(series);
        g2.setPaint(paint);
        g2.setStroke(getSeriesOutlineStroke(series));
        g2.draw(polygon);

        // Lastly, fill the web polygon if this is required

        if (this.webFilled) {
            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.1f));
            g2.fill(polygon);
            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, getForegroundAlpha()));
        }
    }

    /**
     * Returns the value to be plotted at the interseries of the
     * series and the category.  This allows us to plot
     * <code>BY_ROW</code> or <code>BY_COLUMN</code> which basically is just
     * reversing the definition of the categories and data series being
     * plotted.
     *
     * @param series the series to be plotted.
     * @param cat the category within the series to be plotted.
     *
     * @return The value to be plotted (possibly <code>null</code>).
     *
     * @see #getDataExtractOrder()
     */
    protected Number getPlotValue(int series, int cat) {
        Number value = null;
        if (this.dataExtractOrder == TableOrder.BY_ROW) {
            value = this.dataset.getValue(series, cat);
        } else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
            value = this.dataset.getValue(cat, series);
        }
        return value;
    }

    /**
     * Draws the label for one axis.
     *
     * @param g2  the graphics device.
     * @param plotArea  whole plot drawing area (e.g. including space for labels)
     * @param plotDrawingArea  the plot drawing area (just spanning of axis)
     * @param value  the value of the label (ignored).
     * @param cat  the category (zero-based index).
     * @param startAngle  the starting angle.
     * @param extent  the extent of the arc.
     */
    protected void drawLabel(Graphics2D g2, Rectangle2D plotArea, Rectangle2D plotDrawingArea, double value,
            int cat, double startAngle, double extent) {
        FontRenderContext frc = g2.getFontRenderContext();

        String label = null;
        if (this.dataExtractOrder == TableOrder.BY_ROW) {
            // if series are in rows, then the categories are the column keys
            label = this.labelGenerator.generateColumnLabel(this.dataset, cat);
        } else {
            // if series are in columns, then the categories are the row keys
            label = this.labelGenerator.generateRowLabel(this.dataset, cat);
        }

        double angle = normalize(startAngle);

        Font font = getLabelFont();
        Point2D labelLocation;
        do {
            Rectangle2D labelBounds = font.getStringBounds(label, frc);
            LineMetrics lm = font.getLineMetrics(label, frc);
            double ascent = lm.getAscent();

            labelLocation = calculateLabelLocation(labelBounds, ascent, plotDrawingArea, startAngle);

            boolean leftOut = angle > 90 && angle < 270 && labelLocation.getX() < plotArea.getX();
            boolean rightOut = (angle < 90 || angle > 270)
                    && labelLocation.getX() + labelBounds.getWidth() > plotArea.getX() + plotArea.getWidth();

            if (leftOut || rightOut) {
                font = font.deriveFont(font.getSize2D() - 1);
            } else {
                break;
            }
        } while (font.getSize() > 8);

        Composite saveComposite = g2.getComposite();

        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
        g2.setPaint(getLabelPaint());
        g2.setFont(font);
        g2.drawString(label, (float) labelLocation.getX(), (float) labelLocation.getY());
        g2.setComposite(saveComposite);
    }

    /**
     * Returns the location for a label
     *
     * @param labelBounds the label bounds.
     * @param ascent the ascent (height of font).
     * @param plotArea the plot area
     * @param startAngle the start angle for the pie series.
     *
     * @return The location for a label.
     */
    protected Point2D calculateLabelLocation(Rectangle2D labelBounds, double ascent, Rectangle2D plotArea,
            double startAngle) {
        Arc2D arc1 = new Arc2D.Double(plotArea, startAngle, 0, Arc2D.OPEN);
        Point2D point1 = arc1.getEndPoint();

        double deltaX = -(point1.getX() - plotArea.getCenterX()) * this.axisLabelGap;
        double deltaY = -(point1.getY() - plotArea.getCenterY()) * this.axisLabelGap;

        double labelX = point1.getX() - deltaX;
        double labelY = point1.getY() - deltaY;

        if (labelX < plotArea.getCenterX()) {
            labelX -= labelBounds.getWidth();
        }

        if (labelX == plotArea.getCenterX()) {
            labelX -= labelBounds.getWidth() / 2;
        }

        if (labelY > plotArea.getCenterY()) {
            labelY += ascent;
        }

        return new Point2D.Double(labelX, labelY);
    }

    private double normalize(double _axisAngle) {
        double axisAngle = _axisAngle % 360;
        if (axisAngle < 0) {
            axisAngle += 360;
        }
        return axisAngle;
    }

    /**
     * Tests this plot for equality with an arbitrary 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 RadarPlot)) {
            return false;
        }
        if (!super.equals(obj)) {
            return false;
        }
        RadarPlot that = (RadarPlot) obj;
        if (!this.dataExtractOrder.equals(that.dataExtractOrder)) {
            return false;
        }
        if (this.headPercent != that.headPercent) {
            return false;
        }
        if (this.interiorGap != that.interiorGap) {
            return false;
        }
        if (this.startAngle != that.startAngle) {
            return false;
        }
        if (!this.direction.equals(that.direction)) {
            return false;
        }
        if (!ObjectUtilities.equal(this.maxValue, that.maxValue)) {
            return false;
        }
        if (!ObjectUtilities.equal(this.maxValues, that.maxValues)) {
            return false;
        }
        if (!ObjectUtilities.equal(this.origins, that.origins)) {
            return false;
        }
        if (this.webFilled != that.webFilled) {
            return false;
        }
        if (this.axisLabelGap != that.axisLabelGap) {
            return false;
        }
        if (!PaintUtilities.equal(this.axisLinePaint, that.axisLinePaint)) {
            return false;
        }
        if (!this.axisLineStroke.equals(that.axisLineStroke)) {
            return false;
        }
        if (!ShapeUtilities.equal(this.legendItemShape, that.legendItemShape)) {
            return false;
        }
        if (!PaintUtilities.equal(this.seriesPaint, that.seriesPaint)) {
            return false;
        }
        if (!this.seriesPaintList.equals(that.seriesPaintList)) {
            return false;
        }
        if (!PaintUtilities.equal(this.baseSeriesPaint, that.baseSeriesPaint)) {
            return false;
        }
        if (!PaintUtilities.equal(this.seriesOutlinePaint, that.seriesOutlinePaint)) {
            return false;
        }
        if (!this.seriesOutlinePaintList.equals(that.seriesOutlinePaintList)) {
            return false;
        }
        if (!PaintUtilities.equal(this.baseSeriesOutlinePaint, that.baseSeriesOutlinePaint)) {
            return false;
        }
        if (!ObjectUtilities.equal(this.seriesOutlineStroke, that.seriesOutlineStroke)) {
            return false;
        }
        if (!this.seriesOutlineStrokeList.equals(that.seriesOutlineStrokeList)) {
            return false;
        }
        if (!this.baseSeriesOutlineStroke.equals(that.baseSeriesOutlineStroke)) {
            return false;
        }
        if (!this.labelFont.equals(that.labelFont)) {
            return false;
        }
        if (!PaintUtilities.equal(this.labelPaint, that.labelPaint)) {
            return false;
        }
        if (!this.labelGenerator.equals(that.labelGenerator)) {
            return false;
        }
        if (!ObjectUtilities.equal(this.toolTipGenerator, that.toolTipGenerator)) {
            return false;
        }
        if (!ObjectUtilities.equal(this.urlGenerator, that.urlGenerator)) {
            return false;
        }
        if (!ObjectUtilities.equal(this.headOutlineStroke, that.headOutlineStroke)) {
            return false;
        }
        return true;
    }

    /**
     * Returns a clone of this plot.
     *
     * @return A clone of this plot.
     *
     * @throws CloneNotSupportedException if the plot cannot be cloned for
     *         any reason.
     */
    public Object clone() throws CloneNotSupportedException {
        RadarPlot clone = (RadarPlot) super.clone();
        clone.legendItemShape = ShapeUtilities.clone(this.legendItemShape);
        clone.seriesPaintList = (PaintList) this.seriesPaintList.clone();
        clone.seriesOutlinePaintList = (PaintList) this.seriesOutlinePaintList.clone();
        clone.seriesOutlineStrokeList = (StrokeList) this.seriesOutlineStrokeList.clone();
        return clone;
    }

    /**
     * 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.legendItemShape, stream);
        SerialUtilities.writePaint(this.seriesPaint, stream);
        SerialUtilities.writePaint(this.baseSeriesPaint, stream);
        SerialUtilities.writePaint(this.seriesOutlinePaint, stream);
        SerialUtilities.writePaint(this.baseSeriesOutlinePaint, stream);
        SerialUtilities.writeStroke(this.seriesOutlineStroke, stream);
        SerialUtilities.writeStroke(this.baseSeriesOutlineStroke, stream);
        SerialUtilities.writePaint(this.labelPaint, stream);
        SerialUtilities.writePaint(this.axisLinePaint, stream);
        SerialUtilities.writeStroke(this.axisLineStroke, stream);
    }

    /**
     * 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.legendItemShape = SerialUtilities.readShape(stream);
        this.seriesPaint = SerialUtilities.readPaint(stream);
        this.baseSeriesPaint = SerialUtilities.readPaint(stream);
        this.seriesOutlinePaint = SerialUtilities.readPaint(stream);
        this.baseSeriesOutlinePaint = SerialUtilities.readPaint(stream);
        this.seriesOutlineStroke = SerialUtilities.readStroke(stream);
        this.baseSeriesOutlineStroke = SerialUtilities.readStroke(stream);
        this.labelPaint = SerialUtilities.readPaint(stream);
        this.axisLinePaint = SerialUtilities.readPaint(stream);
        this.axisLineStroke = SerialUtilities.readStroke(stream);
        if (this.dataset != null) {
            this.dataset.addChangeListener(this);
        }
    }

}