Java tutorial
/* =========================================================== * 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); } } }