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