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
059package org.jfree.chart.renderer.category;
060
061import java.awt.Graphics2D;
062import java.awt.Paint;
063import java.awt.Shape;
064import java.awt.Stroke;
065import java.awt.geom.Line2D;
066import java.awt.geom.Rectangle2D;
067import java.io.IOException;
068import java.io.ObjectInputStream;
069import java.io.ObjectOutputStream;
070import java.io.Serializable;
071
072import org.jfree.chart.HashUtilities;
073import org.jfree.chart.axis.CategoryAxis;
074import org.jfree.chart.axis.ValueAxis;
075import org.jfree.chart.entity.EntityCollection;
076import org.jfree.chart.event.RendererChangeEvent;
077import org.jfree.chart.plot.CategoryPlot;
078import org.jfree.chart.plot.PlotOrientation;
079import org.jfree.data.Range;
080import org.jfree.data.category.CategoryDataset;
081import org.jfree.data.statistics.StatisticalCategoryDataset;
082import org.jfree.io.SerialUtilities;
083import org.jfree.ui.RectangleEdge;
084import org.jfree.util.ObjectUtilities;
085import org.jfree.util.PaintUtilities;
086import org.jfree.util.PublicCloneable;
087import 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 */
099public 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}