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    }