    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     * StatisticalLineAndShapeRenderer.java
    029     * ------------------------------------
    030     * (C) Copyright 2005-2009, by Object Refinery Limited and Contributors.
    031     *
    032     * Original Author:  Mofeed Shahin;
    033     * Contributor(s):   David Gilbert (for Object Refinery Limited);
    034     *                   Peter Kolb (patch 2497611);
    035     *
    036     * Changes
    037     * -------
    038     * 01-Feb-2005 : Version 1, contributed by Mofeed Shahin (DG);
    039     * 16-Jun-2005 : Added errorIndicatorPaint to be consistent with
    040     *               StatisticalBarRenderer (DG);
    041     * ------------- JFREECHART 1.0.x ---------------------------------------------
    042     * 11-Apr-2006 : Fixed bug 1468794, error bars drawn incorrectly when rendering
    043     *               plots with horizontal orientation (DG);
    044     * 25-Sep-2006 : Fixed bug 1562759, constructor ignoring arguments (DG);
    045     * 01-Jun-2007 : Return early from drawItem() method if item is not
    046     *               visible (DG);
    047     * 14-Jun-2007 : If the dataset is not a StatisticalCategoryDataset, revert
    048     *               to the drawing behaviour of LineAndShapeRenderer (DG);
    049     * 27-Sep-2007 : Added offset option to match new option in
    050     *               LineAndShapeRenderer (DG);
    051     * 14-Jan-2009 : Added support for seriesVisible flags (PK);
    052     * 23-Jan-2009 : Observe useFillPaint and drawOutlines flags (PK);
    053     * 23-Jan-2009 : In drawItem, divide code into passes (DG);
    054     * 05-Feb-2009 : Added errorIndicatorStroke field (DG);
    055     * 01-Apr-2009 : Added override for findRangeBounds(), and fixed NPE in
    056     *               creating item entities (DG);
    057     */
    058
    059    package org.jfree.chart.renderer.category;
    060
    061    import java.awt.Graphics2D;
    062    import java.awt.Paint;
    063    import java.awt.Shape;
    064    import java.awt.Stroke;
    065    import java.awt.geom.Line2D;
    066    import java.awt.geom.Rectangle2D;
    067    import java.io.IOException;
    068    import java.io.ObjectInputStream;
    069    import java.io.ObjectOutputStream;
    070    import java.io.Serializable;
    071
    072    import org.jfree.chart.HashUtilities;
    073    import org.jfree.chart.axis.CategoryAxis;
    074    import org.jfree.chart.axis.ValueAxis;
    075    import org.jfree.chart.entity.EntityCollection;
    076    import org.jfree.chart.event.RendererChangeEvent;
    077    import org.jfree.chart.plot.CategoryPlot;
    078    import org.jfree.chart.plot.PlotOrientation;
    079    import org.jfree.data.Range;
    080    import org.jfree.data.category.CategoryDataset;
    081    import org.jfree.data.statistics.StatisticalCategoryDataset;
    082    import org.jfree.io.SerialUtilities;
    083    import org.jfree.ui.RectangleEdge;
    084    import org.jfree.util.ObjectUtilities;
    085    import org.jfree.util.PaintUtilities;
    086    import org.jfree.util.PublicCloneable;
    087    import org.jfree.util.ShapeUtilities;
    088
    089    /**
    090     * A renderer that draws shapes for each data item, and lines between data
    091     * items.  Each point has a mean value and a standard deviation line. For use
    092     * with the {@link CategoryPlot} class.  The example shown
    093     * here is generated by the <code>StatisticalLineChartDemo1.java</code> program
    094     * included in the JFreeChart Demo Collection:
    095     * <br><br>
    096     * <img src="../../../../../images/StatisticalLineRendererSample.png"
    097     * alt="StatisticalLineRendererSample.png" />
    098     */
    099    public class StatisticalLineAndShapeRenderer extends LineAndShapeRenderer
    100            implements Cloneable, PublicCloneable, Serializable {
    101
    102        /** For serialization. */
    103        private static final long serialVersionUID = -3557517173697777579L;
    104
    105        /** The paint used to show the error indicator. */
    106        private transient Paint errorIndicatorPaint;
    107
    108        /**
    109         * The stroke used to draw the error indicators.  If null, the renderer
    110         * will use the itemOutlineStroke.
    111         *
    112         * @since 1.0.13
    113         */
    114        private transient Stroke errorIndicatorStroke;
    115
    116        /**
    117         * Constructs a default renderer (draws shapes and lines).
    118         */
    119        public StatisticalLineAndShapeRenderer() {
    120            this(true, true);
    121        }
    122
    123        /**
    124         * Constructs a new renderer.
    125         *
    126         * @param linesVisible  draw lines?
    127         * @param shapesVisible  draw shapes?
    128         */
    129        public StatisticalLineAndShapeRenderer(boolean linesVisible,
    130                                               boolean shapesVisible) {
    131            super(linesVisible, shapesVisible);
    132            this.errorIndicatorPaint = null;
    133            this.errorIndicatorStroke = null;
    134        }
    135
    136        /**
    137         * Returns the paint used for the error indicators.
    138         *
    139         * @return The paint used for the error indicators (possibly
    140         *         <code>null</code>).
    141         *
    142         * @see #setErrorIndicatorPaint(Paint)
    143         */
    144        public Paint getErrorIndicatorPaint() {
    145            return this.errorIndicatorPaint;
    146        }
    147
    148        /**
    149         * Sets the paint used for the error indicators (if <code>null</code>,
    150         * the item paint is used instead) and sends a
    151         * {@link RendererChangeEvent} to all registered listeners.
    152         *
    153         * @param paint  the paint (<code>null</code> permitted).
    154         *
    155         * @see #getErrorIndicatorPaint()
    156         */
    157        public void setErrorIndicatorPaint(Paint paint) {
    158            this.errorIndicatorPaint = paint;
    159            fireChangeEvent();
    160        }
    161
    162        /**
    163         * Returns the stroke used for the error indicators.
    164         *
    165         * @return The stroke used for the error indicators (possibly
    166         *         <code>null</code>).
    167         *
    168         * @see #setErrorIndicatorStroke(Stroke)
    169         *
    170         * @since 1.0.13
    171         */
    172        public Stroke getErrorIndicatorStroke() {
    173            return this.errorIndicatorStroke;
    174        }
    175
    176        /**
    177         * Sets the stroke used for the error indicators (if <code>null</code>,
    178         * the item outline stroke is used instead) and sends a
    179         * {@link RendererChangeEvent} to all registered listeners.
    180         *
    181         * @param stroke  the stroke (<code>null</code> permitted).
    182         *
    183         * @see #getErrorIndicatorStroke()
    184         *
    185         * @since 1.0.13
    186         */
    187        public void setErrorIndicatorStroke(Stroke stroke) {
    188            this.errorIndicatorStroke = stroke;
    189            fireChangeEvent();
    190        }
    191
    192        /**
    193         * Returns the range of values the renderer requires to display all the
    194         * items from the specified dataset.
    195         *
    196         * @param dataset  the dataset (<code>null</code> permitted).
    197         *
    198         * @return The range (or <code>null</code> if the dataset is
    199         *         <code>null</code> or empty).
    200         */
    201        public Range findRangeBounds(CategoryDataset dataset) {
    202            return findRangeBounds(dataset, true);
    203        }
    204
    205        /**
    206         * Draw a single data item.
    207         *
    208         * @param g2  the graphics device.
    209         * @param state  the renderer state.
    210         * @param dataArea  the area in which the data is drawn.
    211         * @param plot  the plot.
    212         * @param domainAxis  the domain axis.
    213         * @param rangeAxis  the range axis.
    214         * @param dataset  the dataset (a {@link StatisticalCategoryDataset} is
    215         *                 required).
    216         * @param row  the row index (zero-based).
    217         * @param column  the column index (zero-based).
    218         * @param pass  the pass.
    219         */
    220        public void drawItem(Graphics2D g2,
    221                             CategoryItemRendererState state,
    222                             Rectangle2D dataArea,
    223                             CategoryPlot plot,
    224                             CategoryAxis domainAxis,
    225                             ValueAxis rangeAxis,
    226                             CategoryDataset dataset,
    227                             int row,
    228                             int column,
    229                             int pass) {
    230
    231            // do nothing if item is not visible
    232            if (!getItemVisible(row, column)) {
    233                return;
    234            }
    235
    236            // if the dataset is not a StatisticalCategoryDataset then just revert
    237            // to the superclass (LineAndShapeRenderer) behaviour...
    238            if (!(dataset instanceof StatisticalCategoryDataset)) {
    239                super.drawItem(g2, state, dataArea, plot, domainAxis, rangeAxis,
    240                        dataset, row, column, pass);
    241                return;
    242            }
    243
    244            int visibleRow = state.getVisibleSeriesIndex(row);
    245            if (visibleRow < 0) {
    246                return;
    247            }
    248                    int visibleRowCount = state.getVisibleSeriesCount();
    249
    250            StatisticalCategoryDataset statDataset
    251                    = (StatisticalCategoryDataset) dataset;
    252            Number meanValue = statDataset.getMeanValue(row, column);
    253            if (meanValue == null) {
    254                return;
    255            }
    256            PlotOrientation orientation = plot.getOrientation();
    257
    258            // current data point...
    259            double x1;
    260            if (getUseSeriesOffset()) {
    261                x1 = domainAxis.getCategorySeriesMiddle(column,
    262                        dataset.getColumnCount(),
    263                                            visibleRow, visibleRowCount,
    264                        getItemMargin(), dataArea, plot.getDomainAxisEdge());
    265            }
    266            else {
    267                x1 = domainAxis.getCategoryMiddle(column, getColumnCount(),
    268                        dataArea, plot.getDomainAxisEdge());
    269            }
    270            double y1 = rangeAxis.valueToJava2D(meanValue.doubleValue(), dataArea,
    271                    plot.getRangeAxisEdge());
    272
    273            // draw the standard deviation lines *before* the shapes (if they're
    274            // visible) - it looks better if the shape fill colour is different to
    275            // the line colour
    276            Number sdv = statDataset.getStdDevValue(row, column);
    277            if (pass == 1 && sdv != null) {
    278                //standard deviation lines
    279                RectangleEdge yAxisLocation = plot.getRangeAxisEdge();
    280                double valueDelta = sdv.doubleValue();
    281                double highVal, lowVal;
    282                if ((meanValue.doubleValue() + valueDelta)
    283                        > rangeAxis.getRange().getUpperBound()) {
    284                    highVal = rangeAxis.valueToJava2D(
    285                            rangeAxis.getRange().getUpperBound(), dataArea,
    286                            yAxisLocation);
    287                }
    288                else {
    289                    highVal = rangeAxis.valueToJava2D(meanValue.doubleValue()
    290                            + valueDelta, dataArea, yAxisLocation);
    291                }
    292
    293                if ((meanValue.doubleValue() + valueDelta)
    294                        < rangeAxis.getRange().getLowerBound()) {
    295                    lowVal = rangeAxis.valueToJava2D(
    296                            rangeAxis.getRange().getLowerBound(), dataArea,
    297                            yAxisLocation);
    298                }
    299                else {
    300                    lowVal = rangeAxis.valueToJava2D(meanValue.doubleValue()
    301                            - valueDelta, dataArea, yAxisLocation);
    302                }
    303
    304                if (this.errorIndicatorPaint != null) {
    305                    g2.setPaint(this.errorIndicatorPaint);
    306                }
    307                else {
    308                    g2.setPaint(getItemPaint(row, column));
    309                }
    310                if (this.errorIndicatorStroke != null) {
    311                    g2.setStroke(this.errorIndicatorStroke);
    312                }
    313                else {
    314                    g2.setStroke(getItemOutlineStroke(row, column));
    315                }
    316                Line2D line = new Line2D.Double();
    317                if (orientation == PlotOrientation.HORIZONTAL) {
    318                    line.setLine(lowVal, x1, highVal, x1);
    319                    g2.draw(line);
    320                    line.setLine(lowVal, x1 - 5.0d, lowVal, x1 + 5.0d);
    321                    g2.draw(line);
    322                    line.setLine(highVal, x1 - 5.0d, highVal, x1 + 5.0d);
    323                    g2.draw(line);
    324                }
    325                else {  // PlotOrientation.VERTICAL
    326                    line.setLine(x1, lowVal, x1, highVal);
    327                    g2.draw(line);
    328                    line.setLine(x1 - 5.0d, highVal, x1 + 5.0d, highVal);
    329                    g2.draw(line);
    330                    line.setLine(x1 - 5.0d, lowVal, x1 + 5.0d, lowVal);
    331                    g2.draw(line);
    332                }
    333
    334            }
    335
    336            Shape hotspot = null;
    337            if (pass == 1 && getItemShapeVisible(row, column)) {
    338                Shape shape = getItemShape(row, column);
    339                if (orientation == PlotOrientation.HORIZONTAL) {
    340                    shape = ShapeUtilities.createTranslatedShape(shape, y1, x1);
    341                }
    342                else if (orientation == PlotOrientation.VERTICAL) {
    343                    shape = ShapeUtilities.createTranslatedShape(shape, x1, y1);
    344                }
    345                hotspot = shape;
    346
    347                if (getItemShapeFilled(row, column)) {
    348                    if (getUseFillPaint()) {
    349                        g2.setPaint(getItemFillPaint(row, column));
    350                    }
    351                    else {
    352                        g2.setPaint(getItemPaint(row, column));
    353                    }
    354                    g2.fill(shape);
    355                }
    356                if (getDrawOutlines()) {
    357                    if (getUseOutlinePaint()) {
    358                        g2.setPaint(getItemOutlinePaint(row, column));
    359                    }
    360                    else {
    361                        g2.setPaint(getItemPaint(row, column));
    362                    }
    363                    g2.setStroke(getItemOutlineStroke(row, column));
    364                    g2.draw(shape);
    365                }
    366                // draw the item label if there is one...
    367                if (isItemLabelVisible(row, column)) {
    368                    if (orientation == PlotOrientation.HORIZONTAL) {
    369                        drawItemLabel(g2, orientation, dataset, row, column,
    370                                y1, x1, (meanValue.doubleValue() < 0.0));
    371                    }
    372                    else if (orientation == PlotOrientation.VERTICAL) {
    373                        drawItemLabel(g2, orientation, dataset, row, column,
    374                                x1, y1, (meanValue.doubleValue() < 0.0));
    375                    }
    376                }
    377            }
    378
    379            if (pass == 0 && getItemLineVisible(row, column)) {
    380                if (column != 0) {
    381
    382                    Number previousValue = statDataset.getValue(row, column - 1);
    383                    if (previousValue != null) {
    384
    385                        // previous data point...
    386                        double previous = previousValue.doubleValue();
    387                        double x0;
    388                        if (getUseSeriesOffset()) {
    389                            x0 = domainAxis.getCategorySeriesMiddle(
    390                                    column - 1, dataset.getColumnCount(),
    391                                    visibleRow, visibleRowCount,
    392                                    getItemMargin(), dataArea,
    393                                    plot.getDomainAxisEdge());
    394                        }
    395                        else {
    396                            x0 = domainAxis.getCategoryMiddle(column - 1,
    397                                    getColumnCount(), dataArea,
    398                                    plot.getDomainAxisEdge());
    399                        }
    400                        double y0 = rangeAxis.valueToJava2D(previous, dataArea,
    401                                plot.getRangeAxisEdge());
    402
    403                        Line2D line = null;
    404                        if (orientation == PlotOrientation.HORIZONTAL) {
    405                            line = new Line2D.Double(y0, x0, y1, x1);
    406                        }
    407                        else if (orientation == PlotOrientation.VERTICAL) {
    408                            line = new Line2D.Double(x0, y0, x1, y1);
    409                        }
    410                        g2.setPaint(getItemPaint(row, column));
    411                        g2.setStroke(getItemStroke(row, column));
    412                        g2.draw(line);
    413                    }
    414                }
    415            }
    416
    417            if (pass == 1) {
    418                // add an item entity, if this information is being collected
    419                EntityCollection entities = state.getEntityCollection();
    420                if (entities != null) {
    421                    addEntity(entities, hotspot, dataset, row, column, x1, y1);
    422                }
    423            }
    424
    425        }
    426
    427        /**
    428         * Tests this renderer for equality with an arbitrary object.
    429         *
    430         * @param obj  the object (<code>null</code> permitted).
    431         *
    432         * @return A boolean.
    433         */
    434        public boolean equals(Object obj) {
    435            if (obj == this) {
    436                return true;
    437            }
    438            if (!(obj instanceof StatisticalLineAndShapeRenderer)) {
    439                return false;
    440            }
    441            StatisticalLineAndShapeRenderer that
    442                    = (StatisticalLineAndShapeRenderer) obj;
    443            if (!PaintUtilities.equal(this.errorIndicatorPaint,
    444                    that.errorIndicatorPaint)) {
    445                return false;
    446            }
    447            if (!ObjectUtilities.equal(this.errorIndicatorStroke,
    448                    that.errorIndicatorStroke)) {
    449                return false;
    450            }
    451            return super.equals(obj);
    452        }
    453
    454        /**
    455         * Returns a hash code for this instance.
    456         *
    457         * @return A hash code.
    458         */
    459        public int hashCode() {
    460            int hash = super.hashCode();
    461            hash = HashUtilities.hashCode(hash, this.errorIndicatorPaint);
    462            return hash;
    463        }
    464
    465        /**
    466         * Provides serialization support.
    467         *
    468         * @param stream  the output stream.
    469         *
    470         * @throws IOException  if there is an I/O error.
    471         */
    472        private void writeObject(ObjectOutputStream stream) throws IOException {
    473            stream.defaultWriteObject();
    474            SerialUtilities.writePaint(this.errorIndicatorPaint, stream);
    475            SerialUtilities.writeStroke(this.errorIndicatorStroke, stream);
    476        }
    477
    478        /**
    479         * Provides serialization support.
    480         *
    481         * @param stream  the input stream.
    482         *
    483         * @throws IOException  if there is an I/O error.
    484         * @throws ClassNotFoundException  if there is a classpath problem.
    485         */
    486        private void readObject(ObjectInputStream stream)
    487                throws IOException, ClassNotFoundException {
    488            stream.defaultReadObject();
    489            this.errorIndicatorPaint = SerialUtilities.readPaint(stream);
    490            this.errorIndicatorStroke = SerialUtilities.readStroke(stream);
    491        }
    492
    493    }