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