    001    /* ===========================================================
    002     * JFreeChart : a free chart library for the Java(tm) platform
    003     * ===========================================================
    004     *
    005     * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors.
    006     *
    007     * Project Info:  http://www.jfree.org/jfreechart/index.html
    008     *
    009     * This library is free software; you can redistribute it and/or modify it
    010     * under the terms of the GNU Lesser General Public License as published by
    011     * the Free Software Foundation; either version 2.1 of the License, or
    012     * (at your option) any later version.
    013     *
    014     * This library is distributed in the hope that it will be useful, but
    015     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
    016     * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
    017     * License for more details.
    018     *
    019     * You should have received a copy of the GNU Lesser General Public
    020     * License along with this library; if not, write to the Free Software
    021     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
    022     * USA.
    023     *
    024     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
    025     * in the United States and other countries.]
    026     *
    027     * --------------------
    028     * ScatterRenderer.java
    029     * --------------------
    030     * (C) Copyright 2007, 2008, by Object Refinery Limited and Contributors.
    031     *
    032     * Original Author:  David Gilbert (for Object Refinery Limited);
    033     * Contributor(s):   David Forslund;
    034     *
    035     * Changes
    036     * -------
    037     * 08-Oct-2007 : Version 1, based on patch 1780779 by David Forslund (DG);
    038     * 11-Oct-2007 : Renamed ScatterRenderer (DG);
    039     *
    040     */
    041
    042    package org.jfree.chart.renderer.category;
    043
    044    import java.awt.Graphics2D;
    045    import java.awt.Paint;
    046    import java.awt.Shape;
    047    import java.awt.Stroke;
    048    import java.awt.geom.Line2D;
    049    import java.awt.geom.Rectangle2D;
    050    import java.io.IOException;
    051    import java.io.ObjectInputStream;
    052    import java.io.ObjectOutputStream;
    053    import java.io.Serializable;
    054    import java.util.List;
    055
    056    import org.jfree.chart.LegendItem;
    057    import org.jfree.chart.axis.CategoryAxis;
    058    import org.jfree.chart.axis.ValueAxis;
    059    import org.jfree.chart.event.RendererChangeEvent;
    060    import org.jfree.chart.plot.CategoryPlot;
    061    import org.jfree.chart.plot.PlotOrientation;
    062    import org.jfree.data.category.CategoryDataset;
    063    import org.jfree.data.statistics.MultiValueCategoryDataset;
    064    import org.jfree.util.BooleanList;
    065    import org.jfree.util.BooleanUtilities;
    066    import org.jfree.util.ObjectUtilities;
    067    import org.jfree.util.PublicCloneable;
    068    import org.jfree.util.ShapeUtilities;
    069
    070    /**
    071     * A renderer that handles the multiple values from a
    072     * {@link MultiValueCategoryDataset} by plotting a shape for each value for
    073     * each given item in the dataset.
    074     *
    075     * @since 1.0.7
    076     */
    077    public class ScatterRenderer extends AbstractCategoryItemRenderer
    078            implements Cloneable, PublicCloneable, Serializable {
    079
    080        /**
    081         * A table of flags that control (per series) whether or not shapes are
    082         * filled.
    083         */
    084        private BooleanList seriesShapesFilled;
    085
    086        /**
    087         * The default value returned by the getShapeFilled() method.
    088         */
    089        private boolean baseShapesFilled;
    090
    091        /**
    092         * A flag that controls whether the fill paint is used for filling
    093         * shapes.
    094         */
    095        private boolean useFillPaint;
    096
    097        /**
    098         * A flag that controls whether outlines are drawn for shapes.
    099         */
    100        private boolean drawOutlines;
    101
    102        /**
    103         * A flag that controls whether the outline paint is used for drawing shape
    104         * outlines - if not, the regular series paint is used.
    105         */
    106        private boolean useOutlinePaint;
    107
    108        /**
    109         * A flag that controls whether or not the x-position for each item is
    110         * offset within the category according to the series.
    111         */
    112        private boolean useSeriesOffset;
    113
    114        /**
    115         * The item margin used for series offsetting - this allows the positioning
    116         * to match the bar positions of the {@link BarRenderer} class.
    117         */
    118        private double itemMargin;
    119
    120        /**
    121         * Constructs a new renderer.
    122         */
    123        public ScatterRenderer() {
    124            this.seriesShapesFilled = new BooleanList();
    125            this.baseShapesFilled = true;
    126            this.useFillPaint = false;
    127            this.drawOutlines = false;
    128            this.useOutlinePaint = false;
    129            this.useSeriesOffset = true;
    130            this.itemMargin = 0.20;
    131        }
    132
    133        /**
    134         * Returns the flag that controls whether or not the x-position for each
    135         * data item is offset within the category according to the series.
    136         *
    137         * @return A boolean.
    138         *
    139         * @see #setUseSeriesOffset(boolean)
    140         */
    141        public boolean getUseSeriesOffset() {
    142            return this.useSeriesOffset;
    143        }
    144
    145        /**
    146         * Sets the flag that controls whether or not the x-position for each
    147         * data item is offset within its category according to the series, and
    148         * sends a {@link RendererChangeEvent} to all registered listeners.
    149         *
    150         * @param offset  the offset.
    151         *
    152         * @see #getUseSeriesOffset()
    153         */
    154        public void setUseSeriesOffset(boolean offset) {
    155            this.useSeriesOffset = offset;
    156            fireChangeEvent();
    157        }
    158
    159        /**
    160         * Returns the item margin, which is the gap between items within a
    161         * category (expressed as a percentage of the overall category width).
    162         * This can be used to match the offset alignment with the bars drawn by
    163         * a {@link BarRenderer}).
    164         *
    165         * @return The item margin.
    166         *
    167         * @see #setItemMargin(double)
    168         * @see #getUseSeriesOffset()
    169         */
    170        public double getItemMargin() {
    171            return this.itemMargin;
    172        }
    173
    174        /**
    175         * Sets the item margin, which is the gap between items within a category
    176         * (expressed as a percentage of the overall category width), and sends
    177         * a {@link RendererChangeEvent} to all registered listeners.
    178         *
    179         * @param margin  the margin (0.0 <= margin < 1.0).
    180         *
    181         * @see #getItemMargin()
    182         * @see #getUseSeriesOffset()
    183         */
    184        public void setItemMargin(double margin) {
    185            if (margin < 0.0 || margin >= 1.0) {
    186                throw new IllegalArgumentException("Requires 0.0 <= margin < 1.0.");
    187            }
    188            this.itemMargin = margin;
    189            fireChangeEvent();
    190        }
    191
    192        /**
    193         * Returns <code>true</code> if outlines should be drawn for shapes, and
    194         * <code>false</code> otherwise.
    195         *
    196         * @return A boolean.
    197         *
    198         * @see #setDrawOutlines(boolean)
    199         */
    200        public boolean getDrawOutlines() {
    201            return this.drawOutlines;
    202        }
    203
    204        /**
    205         * Sets the flag that controls whether outlines are drawn for
    206         * shapes, and sends a {@link RendererChangeEvent} to all registered
    207         * listeners.
    208         * <p/>
    209         * In some cases, shapes look better if they do NOT have an outline, but
    210         * this flag allows you to set your own preference.
    211         *
    212         * @param flag the flag.
    213         *
    214         * @see #getDrawOutlines()
    215         */
    216        public void setDrawOutlines(boolean flag) {
    217            this.drawOutlines = flag;
    218            fireChangeEvent();
    219        }
    220
    221        /**
    222         * Returns the flag that controls whether the outline paint is used for
    223         * shape outlines.  If not, the regular series paint is used.
    224         *
    225         * @return A boolean.
    226         *
    227         * @see #setUseOutlinePaint(boolean)
    228         */
    229        public boolean getUseOutlinePaint() {
    230            return this.useOutlinePaint;
    231        }
    232
    233        /**
    234         * Sets the flag that controls whether the outline paint is used for shape
    235         * outlines, and sends a {@link RendererChangeEvent} to all registered
    236         * listeners.
    237         *
    238         * @param use the flag.
    239         *
    240         * @see #getUseOutlinePaint()
    241         */
    242        public void setUseOutlinePaint(boolean use) {
    243            this.useOutlinePaint = use;
    244            fireChangeEvent();
    245        }
    246
    247        // SHAPES FILLED
    248
    249        /**
    250         * Returns the flag used to control whether or not the shape for an item
    251         * is filled. The default implementation passes control to the
    252         * <code>getSeriesShapesFilled</code> method. You can override this method
    253         * if you require different behaviour.
    254         *
    255         * @param series the series index (zero-based).
    256         * @param item   the item index (zero-based).
    257         * @return A boolean.
    258         */
    259        public boolean getItemShapeFilled(int series, int item) {
    260            return getSeriesShapesFilled(series);
    261        }
    262
    263        /**
    264         * Returns the flag used to control whether or not the shapes for a series
    265         * are filled.
    266         *
    267         * @param series the series index (zero-based).
    268         * @return A boolean.
    269         */
    270        public boolean getSeriesShapesFilled(int series) {
    271            Boolean flag = this.seriesShapesFilled.getBoolean(series);
    272            if (flag != null) {
    273                return flag.booleanValue();
    274            }
    275            else {
    276                return this.baseShapesFilled;
    277            }
    278
    279        }
    280
    281        /**
    282         * Sets the 'shapes filled' flag for a series and sends a
    283         * {@link RendererChangeEvent} to all registered listeners.
    284         *
    285         * @param series the series index (zero-based).
    286         * @param filled the flag.
    287         */
    288        public void setSeriesShapesFilled(int series, Boolean filled) {
    289            this.seriesShapesFilled.setBoolean(series, filled);
    290            fireChangeEvent();
    291        }
    292
    293        /**
    294         * Sets the 'shapes filled' flag for a series and sends a
    295         * {@link RendererChangeEvent} to all registered listeners.
    296         *
    297         * @param series the series index (zero-based).
    298         * @param filled the flag.
    299         */
    300        public void setSeriesShapesFilled(int series, boolean filled) {
    301            this.seriesShapesFilled.setBoolean(series,
    302                    BooleanUtilities.valueOf(filled));
    303            fireChangeEvent();
    304        }
    305
    306        /**
    307         * Returns the base 'shape filled' attribute.
    308         *
    309         * @return The base flag.
    310         */
    311        public boolean getBaseShapesFilled() {
    312            return this.baseShapesFilled;
    313        }
    314
    315        /**
    316         * Sets the base 'shapes filled' flag and sends a
    317         * {@link RendererChangeEvent} to all registered listeners.
    318         *
    319         * @param flag the flag.
    320         */
    321        public void setBaseShapesFilled(boolean flag) {
    322            this.baseShapesFilled = flag;
    323            fireChangeEvent();
    324        }
    325
    326        /**
    327         * Returns <code>true</code> if the renderer should use the fill paint
    328         * setting to fill shapes, and <code>false</code> if it should just
    329         * use the regular paint.
    330         *
    331         * @return A boolean.
    332         */
    333        public boolean getUseFillPaint() {
    334            return this.useFillPaint;
    335        }
    336
    337        /**
    338         * Sets the flag that controls whether the fill paint is used to fill
    339         * shapes, and sends a {@link RendererChangeEvent} to all
    340         * registered listeners.
    341         *
    342         * @param flag the flag.
    343         */
    344        public void setUseFillPaint(boolean flag) {
    345            this.useFillPaint = flag;
    346            fireChangeEvent();
    347        }
    348
    349        /**
    350         * Draw a single data item.
    351         *
    352         * @param g2  the graphics device.
    353         * @param state  the renderer state.
    354         * @param dataArea  the area in which the data is drawn.
    355         * @param plot  the plot.
    356         * @param domainAxis  the domain axis.
    357         * @param rangeAxis  the range axis.
    358         * @param dataset  the dataset.
    359         * @param row  the row index (zero-based).
    360         * @param column  the column index (zero-based).
    361         * @param pass  the pass index.
    362         */
    363        public void drawItem(Graphics2D g2, CategoryItemRendererState state,
    364                Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis,
    365                ValueAxis rangeAxis, CategoryDataset dataset, int row, int column,
    366                int pass) {
    367
    368            // do nothing if item is not visible
    369            if (!getItemVisible(row, column)) {
    370                return;
    371            }
    372
    373            PlotOrientation orientation = plot.getOrientation();
    374
    375            MultiValueCategoryDataset d = (MultiValueCategoryDataset) dataset;
    376            List values = d.getValues(row, column);
    377            if (values == null) {
    378                return;
    379            }
    380            int valueCount = values.size();
    381            for (int i = 0; i < valueCount; i++) {
    382                // current data point...
    383                double x1;
    384                if (this.useSeriesOffset) {
    385                    x1 = domainAxis.getCategorySeriesMiddle(dataset.getColumnKey(
    386                            column), dataset.getRowKey(row), dataset,
    387                            this.itemMargin, dataArea, plot.getDomainAxisEdge());
    388                }
    389                else {
    390                    x1 = domainAxis.getCategoryMiddle(column, getColumnCount(),
    391                            dataArea, plot.getDomainAxisEdge());
    392                }
    393                Number n = (Number) values.get(i);
    394                double value = n.doubleValue();
    395                double y1 = rangeAxis.valueToJava2D(value, dataArea,
    396                        plot.getRangeAxisEdge());
    397
    398                Shape shape = getItemShape(row, column);
    399                if (orientation == PlotOrientation.HORIZONTAL) {
    400                    shape = ShapeUtilities.createTranslatedShape(shape, y1, x1);
    401                }
    402                else if (orientation == PlotOrientation.VERTICAL) {
    403                    shape = ShapeUtilities.createTranslatedShape(shape, x1, y1);
    404                }
    405                if (getItemShapeFilled(row, column)) {
    406                    if (this.useFillPaint) {
    407                        g2.setPaint(getItemFillPaint(row, column));
    408                    }
    409                    else {
    410                        g2.setPaint(getItemPaint(row, column));
    411                    }
    412                    g2.fill(shape);
    413                }
    414                if (this.drawOutlines) {
    415                    if (this.useOutlinePaint) {
    416                        g2.setPaint(getItemOutlinePaint(row, column));
    417                    }
    418                    else {
    419                        g2.setPaint(getItemPaint(row, column));
    420                    }
    421                    g2.setStroke(getItemOutlineStroke(row, column));
    422                    g2.draw(shape);
    423                }
    424            }
    425
    426        }
    427
    428        /**
    429         * Returns a legend item for a series.
    430         *
    431         * @param datasetIndex  the dataset index (zero-based).
    432         * @param series  the series index (zero-based).
    433         *
    434         * @return The legend item.
    435         */
    436        public LegendItem getLegendItem(int datasetIndex, int series) {
    437
    438            CategoryPlot cp = getPlot();
    439            if (cp == null) {
    440                return null;
    441            }
    442
    443            if (isSeriesVisible(series) && isSeriesVisibleInLegend(series)) {
    444                CategoryDataset dataset = cp.getDataset(datasetIndex);
    445                String label = getLegendItemLabelGenerator().generateLabel(
    446                        dataset, series);
    447                String description = label;
    448                String toolTipText = null;
    449                if (getLegendItemToolTipGenerator() != null) {
    450                    toolTipText = getLegendItemToolTipGenerator().generateLabel(
    451                            dataset, series);
    452                }
    453                String urlText = null;
    454                if (getLegendItemURLGenerator() != null) {
    455                    urlText = getLegendItemURLGenerator().generateLabel(
    456                            dataset, series);
    457                }
    458                Shape shape = lookupSeriesShape(series);
    459                Paint paint = lookupSeriesPaint(series);
    460                Paint fillPaint = (this.useFillPaint
    461                        ? getItemFillPaint(series, 0) : paint);
    462                boolean shapeOutlineVisible = this.drawOutlines;
    463                Paint outlinePaint = (this.useOutlinePaint
    464                        ? getItemOutlinePaint(series, 0) : paint);
    465                Stroke outlineStroke = lookupSeriesOutlineStroke(series);
    466                LegendItem result = new LegendItem(label, description, toolTipText,
    467                        urlText, true, shape, getItemShapeFilled(series, 0),
    468                        fillPaint, shapeOutlineVisible, outlinePaint, outlineStroke,
    469                        false, new Line2D.Double(-7.0, 0.0, 7.0, 0.0),
    470                        getItemStroke(series, 0), getItemPaint(series, 0));
    471                result.setDataset(dataset);
    472                result.setDatasetIndex(datasetIndex);
    473                result.setSeriesKey(dataset.getRowKey(series));
    474                result.setSeriesIndex(series);
    475                return result;
    476            }
    477            return null;
    478
    479        }
    480
    481        /**
    482         * Tests this renderer for equality with an arbitrary object.
    483         *
    484         * @param obj the object (<code>null</code> permitted).
    485         * @return A boolean.
    486         */
    487        public boolean equals(Object obj) {
    488            if (obj == this) {
    489                return true;
    490            }
    491            if (!(obj instanceof ScatterRenderer)) {
    492                return false;
    493            }
    494            ScatterRenderer that = (ScatterRenderer) obj;
    495            if (!ObjectUtilities.equal(this.seriesShapesFilled,
    496                    that.seriesShapesFilled)) {
    497                return false;
    498            }
    499            if (this.baseShapesFilled != that.baseShapesFilled) {
    500                return false;
    501            }
    502            if (this.useFillPaint != that.useFillPaint) {
    503                return false;
    504            }
    505            if (this.drawOutlines != that.drawOutlines) {
    506                return false;
    507            }
    508            if (this.useOutlinePaint != that.useOutlinePaint) {
    509                return false;
    510            }
    511            if (this.useSeriesOffset != that.useSeriesOffset) {
    512                return false;
    513            }
    514            if (this.itemMargin != that.itemMargin) {
    515                return false;
    516            }
    517            return super.equals(obj);
    518        }
    519
    520        /**
    521         * Returns an independent copy of the renderer.
    522         *
    523         * @return A clone.
    524         *
    525         * @throws CloneNotSupportedException  should not happen.
    526         */
    527        public Object clone() throws CloneNotSupportedException {
    528            ScatterRenderer clone = (ScatterRenderer) super.clone();
    529            clone.seriesShapesFilled
    530                    = (BooleanList) this.seriesShapesFilled.clone();
    531            return clone;
    532        }
    533
    534        /**
    535         * Provides serialization support.
    536         *
    537         * @param stream the output stream.
    538         * @throws java.io.IOException if there is an I/O error.
    539         */
    540        private void writeObject(ObjectOutputStream stream) throws IOException {
    541            stream.defaultWriteObject();
    542
    543        }
    544
    545        /**
    546         * Provides serialization support.
    547         *
    548         * @param stream the input stream.
    549         * @throws java.io.IOException    if there is an I/O error.
    550         * @throws ClassNotFoundException if there is a classpath problem.
    551         */
    552        private void readObject(ObjectInputStream stream)
    553                throws IOException, ClassNotFoundException {
    554            stream.defaultReadObject();
    555
    556        }
    557
    558    }