001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2014, 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 * StatisticalLineAndShapeRenderer.java
029 * ------------------------------------
030 * (C) Copyright 2005-2014, 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    @Override
202    public Range findRangeBounds(CategoryDataset dataset) {
203        return findRangeBounds(dataset, true);
204    }
205
206    /**
207     * Draw a single data item.
208     *
209     * @param g2  the graphics device.
210     * @param state  the renderer state.
211     * @param dataArea  the area in which the data is drawn.
212     * @param plot  the plot.
213     * @param domainAxis  the domain axis.
214     * @param rangeAxis  the range axis.
215     * @param dataset  the dataset (a {@link StatisticalCategoryDataset} is
216     *                 required).
217     * @param row  the row index (zero-based).
218     * @param column  the column index (zero-based).
219     * @param pass  the pass.
220     */
221    @Override
222    public void drawItem(Graphics2D g2, CategoryItemRendererState state,
223            Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis,
224            ValueAxis rangeAxis, CategoryDataset dataset, int row, int column,
225            int pass) {
226
227        // do nothing if item is not visible
228        if (!getItemVisible(row, column)) {
229            return;
230        }
231
232        // if the dataset is not a StatisticalCategoryDataset then just revert
233        // to the superclass (LineAndShapeRenderer) behaviour...
234        if (!(dataset instanceof StatisticalCategoryDataset)) {
235            super.drawItem(g2, state, dataArea, plot, domainAxis, rangeAxis,
236                    dataset, row, column, pass);
237            return;
238        }
239
240        int visibleRow = state.getVisibleSeriesIndex(row);
241        if (visibleRow < 0) {
242            return;
243        }
244        int visibleRowCount = state.getVisibleSeriesCount();
245
246        StatisticalCategoryDataset statDataset
247                = (StatisticalCategoryDataset) dataset;
248        Number meanValue = statDataset.getMeanValue(row, column);
249        if (meanValue == null) {
250            return;
251        }
252        PlotOrientation orientation = plot.getOrientation();
253
254        // current data point...
255        double x1;
256        if (getUseSeriesOffset()) {
257            x1 = domainAxis.getCategorySeriesMiddle(column,
258                    dataset.getColumnCount(),
259                    visibleRow, visibleRowCount,
260                    getItemMargin(), dataArea, plot.getDomainAxisEdge());
261        }
262        else {
263            x1 = domainAxis.getCategoryMiddle(column, getColumnCount(),
264                    dataArea, plot.getDomainAxisEdge());
265        }
266        double y1 = rangeAxis.valueToJava2D(meanValue.doubleValue(), dataArea,
267                plot.getRangeAxisEdge());
268
269        // draw the standard deviation lines *before* the shapes (if they're
270        // visible) - it looks better if the shape fill colour is different to
271        // the line colour
272        Number sdv = statDataset.getStdDevValue(row, column);
273        if (pass == 1 && sdv != null) {
274            //standard deviation lines
275            RectangleEdge yAxisLocation = plot.getRangeAxisEdge();
276            double valueDelta = sdv.doubleValue();
277            double highVal, lowVal;
278            if ((meanValue.doubleValue() + valueDelta)
279                    > rangeAxis.getRange().getUpperBound()) {
280                highVal = rangeAxis.valueToJava2D(
281                        rangeAxis.getRange().getUpperBound(), dataArea,
282                        yAxisLocation);
283            }
284            else {
285                highVal = rangeAxis.valueToJava2D(meanValue.doubleValue()
286                        + valueDelta, dataArea, yAxisLocation);
287            }
288
289            if ((meanValue.doubleValue() + valueDelta)
290                    < rangeAxis.getRange().getLowerBound()) {
291                lowVal = rangeAxis.valueToJava2D(
292                        rangeAxis.getRange().getLowerBound(), dataArea,
293                        yAxisLocation);
294            }
295            else {
296                lowVal = rangeAxis.valueToJava2D(meanValue.doubleValue()
297                        - valueDelta, dataArea, yAxisLocation);
298            }
299
300            if (this.errorIndicatorPaint != null) {
301                g2.setPaint(this.errorIndicatorPaint);
302            }
303            else {
304                g2.setPaint(getItemPaint(row, column));
305            }
306            if (this.errorIndicatorStroke != null) {
307                g2.setStroke(this.errorIndicatorStroke);
308            }
309            else {
310                g2.setStroke(getItemOutlineStroke(row, column));
311            }
312            Line2D line = new Line2D.Double();
313            if (orientation == PlotOrientation.HORIZONTAL) {
314                line.setLine(lowVal, x1, highVal, x1);
315                g2.draw(line);
316                line.setLine(lowVal, x1 - 5.0d, lowVal, x1 + 5.0d);
317                g2.draw(line);
318                line.setLine(highVal, x1 - 5.0d, highVal, x1 + 5.0d);
319                g2.draw(line);
320            }
321            else {  // PlotOrientation.VERTICAL
322                line.setLine(x1, lowVal, x1, highVal);
323                g2.draw(line);
324                line.setLine(x1 - 5.0d, highVal, x1 + 5.0d, highVal);
325                g2.draw(line);
326                line.setLine(x1 - 5.0d, lowVal, x1 + 5.0d, lowVal);
327                g2.draw(line);
328            }
329
330        }
331
332        Shape hotspot = null;
333        if (pass == 1 && getItemShapeVisible(row, column)) {
334            Shape shape = getItemShape(row, column);
335            if (orientation == PlotOrientation.HORIZONTAL) {
336                shape = ShapeUtilities.createTranslatedShape(shape, y1, x1);
337            }
338            else if (orientation == PlotOrientation.VERTICAL) {
339                shape = ShapeUtilities.createTranslatedShape(shape, x1, y1);
340            }
341            hotspot = shape;
342            
343            if (getItemShapeFilled(row, column)) {
344                if (getUseFillPaint()) {
345                    g2.setPaint(getItemFillPaint(row, column));
346                }
347                else {
348                    g2.setPaint(getItemPaint(row, column));
349                }
350                g2.fill(shape);
351            }
352            if (getDrawOutlines()) {
353                if (getUseOutlinePaint()) {
354                    g2.setPaint(getItemOutlinePaint(row, column));
355                }
356                else {
357                    g2.setPaint(getItemPaint(row, column));
358                }
359                g2.setStroke(getItemOutlineStroke(row, column));
360                g2.draw(shape);
361            }
362            // draw the item label if there is one...
363            if (isItemLabelVisible(row, column)) {
364                if (orientation == PlotOrientation.HORIZONTAL) {
365                    drawItemLabel(g2, orientation, dataset, row, column,
366                            y1, x1, (meanValue.doubleValue() < 0.0));
367                }
368                else if (orientation == PlotOrientation.VERTICAL) {
369                    drawItemLabel(g2, orientation, dataset, row, column,
370                            x1, y1, (meanValue.doubleValue() < 0.0));
371                }
372            }
373        }
374
375        if (pass == 0 && getItemLineVisible(row, column)) {
376            if (column != 0) {
377
378                Number previousValue = statDataset.getValue(row, column - 1);
379                if (previousValue != null) {
380
381                    // previous data point...
382                    double previous = previousValue.doubleValue();
383                    double x0;
384                    if (getUseSeriesOffset()) {
385                        x0 = domainAxis.getCategorySeriesMiddle(
386                                column - 1, dataset.getColumnCount(),
387                                visibleRow, visibleRowCount,
388                                getItemMargin(), dataArea,
389                                plot.getDomainAxisEdge());
390                    }
391                    else {
392                        x0 = domainAxis.getCategoryMiddle(column - 1,
393                                getColumnCount(), dataArea,
394                                plot.getDomainAxisEdge());
395                    }
396                    double y0 = rangeAxis.valueToJava2D(previous, dataArea,
397                            plot.getRangeAxisEdge());
398
399                    Line2D line = null;
400                    if (orientation == PlotOrientation.HORIZONTAL) {
401                        line = new Line2D.Double(y0, x0, y1, x1);
402                    }
403                    else if (orientation == PlotOrientation.VERTICAL) {
404                        line = new Line2D.Double(x0, y0, x1, y1);
405                    }
406                    g2.setPaint(getItemPaint(row, column));
407                    g2.setStroke(getItemStroke(row, column));
408                    g2.draw(line);
409                }
410            }
411        }
412
413        if (pass == 1) {
414            // add an item entity, if this information is being collected
415            EntityCollection entities = state.getEntityCollection();
416            if (entities != null) {
417                addEntity(entities, hotspot, dataset, row, column, x1, y1);
418            }
419        }
420
421    }
422
423    /**
424     * Tests this renderer for equality with an arbitrary object.
425     *
426     * @param obj  the object (<code>null</code> permitted).
427     *
428     * @return A boolean.
429     */
430    @Override
431    public boolean equals(Object obj) {
432        if (obj == this) {
433            return true;
434        }
435        if (!(obj instanceof StatisticalLineAndShapeRenderer)) {
436            return false;
437        }
438        StatisticalLineAndShapeRenderer that
439                = (StatisticalLineAndShapeRenderer) obj;
440        if (!PaintUtilities.equal(this.errorIndicatorPaint,
441                that.errorIndicatorPaint)) {
442            return false;
443        }
444        if (!ObjectUtilities.equal(this.errorIndicatorStroke,
445                that.errorIndicatorStroke)) {
446            return false;
447        }
448        return super.equals(obj);
449    }
450
451    /**
452     * Returns a hash code for this instance.
453     *
454     * @return A hash code.
455     */
456    @Override
457    public int hashCode() {
458        int hash = super.hashCode();
459        hash = HashUtilities.hashCode(hash, this.errorIndicatorPaint);
460        return hash;
461    }
462
463    /**
464     * Provides serialization support.
465     *
466     * @param stream  the output stream.
467     *
468     * @throws IOException  if there is an I/O error.
469     */
470    private void writeObject(ObjectOutputStream stream) throws IOException {
471        stream.defaultWriteObject();
472        SerialUtilities.writePaint(this.errorIndicatorPaint, stream);
473        SerialUtilities.writeStroke(this.errorIndicatorStroke, stream);
474    }
475
476    /**
477     * Provides serialization support.
478     *
479     * @param stream  the input stream.
480     *
481     * @throws IOException  if there is an I/O error.
482     * @throws ClassNotFoundException  if there is a classpath problem.
483     */
484    private void readObject(ObjectInputStream stream)
485            throws IOException, ClassNotFoundException {
486        stream.defaultReadObject();
487        this.errorIndicatorPaint = SerialUtilities.readPaint(stream);
488        this.errorIndicatorStroke = SerialUtilities.readStroke(stream);
489    }
490
491}