1 /* ===========================================================
2  * JFreeChart : a free chart library for the Java(tm) platform
3  * ===========================================================
4  *
5  * (C) Copyright 2000-2013, by Object Refinery Limited and Contributors.
6  *
7  * Project Info:  http://www.jfree.org/jfreechart/index.html
8  *
9  * This library is free software; you can redistribute it and/or modify it
10  * under the terms of the GNU Lesser General Public License as published by
11  * the Free Software Foundation; either version 2.1 of the License, or
12  * (at your option) any later version.
13  *
14  * This library is distributed in the hope that it will be useful, but
15  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
17  * License for more details.
18  *
19  * You should have received a copy of the GNU Lesser General Public
20  * License along with this library; if not, write to the Free Software
21  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
22  * USA.
23  *
24  * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
25  * Other names may be trademarks of their respective owners.]
26  *
27  * ------------------------------------
28  * StatisticalLineAndShapeRenderer.java
29  * ------------------------------------
30  * (C) Copyright 2005-2009, by Object Refinery Limited and Contributors.
31  *
32  * Original Author:  Mofeed Shahin;
33  * Contributor(s):   David Gilbert (for Object Refinery Limited);
34  *                   Peter Kolb (patch 2497611);
35  *
36  * Changes
37  * -------
38  * 01-Feb-2005 : Version 1, contributed by Mofeed Shahin (DG);
39  * 16-Jun-2005 : Added errorIndicatorPaint to be consistent with
40  *               StatisticalBarRenderer (DG);
41  * ------------- JFREECHART 1.0.x ---------------------------------------------
42  * 11-Apr-2006 : Fixed bug 1468794, error bars drawn incorrectly when rendering
43  *               plots with horizontal orientation (DG);
44  * 25-Sep-2006 : Fixed bug 1562759, constructor ignoring arguments (DG);
45  * 01-Jun-2007 : Return early from drawItem() method if item is not
46  *               visible (DG);
47  * 14-Jun-2007 : If the dataset is not a StatisticalCategoryDataset, revert
48  *               to the drawing behaviour of LineAndShapeRenderer (DG);
49  * 27-Sep-2007 : Added offset option to match new option in
50  *               LineAndShapeRenderer (DG);
51  * 14-Jan-2009 : Added support for seriesVisible flags (PK);
52  * 23-Jan-2009 : Observe useFillPaint and drawOutlines flags (PK);
53  * 23-Jan-2009 : In drawItem, divide code into passes (DG);
54  * 05-Feb-2009 : Added errorIndicatorStroke field (DG);
55  * 01-Apr-2009 : Added override for findRangeBounds(), and fixed NPE in
56  *               creating item entities (DG);
57  */
58 
59 package org.jfree.chart.renderer.category;
60 
61 import java.awt.Graphics2D;
62 import java.awt.Paint;
63 import java.awt.Shape;
64 import java.awt.Stroke;
65 import java.awt.geom.Line2D;
66 import java.awt.geom.Rectangle2D;
67 import java.io.IOException;
68 import java.io.ObjectInputStream;
69 import java.io.ObjectOutputStream;
70 import java.io.Serializable;
71 
72 import org.jfree.chart.HashUtilities;
73 import org.jfree.chart.axis.CategoryAxis;
74 import org.jfree.chart.axis.ValueAxis;
75 import org.jfree.chart.entity.EntityCollection;
76 import org.jfree.chart.event.RendererChangeEvent;
77 import org.jfree.chart.plot.CategoryPlot;
78 import org.jfree.chart.plot.PlotOrientation;
79 import org.jfree.data.Range;
80 import org.jfree.data.category.CategoryDataset;
81 import org.jfree.data.statistics.StatisticalCategoryDataset;
82 import org.jfree.io.SerialUtilities;
83 import org.jfree.ui.RectangleEdge;
84 import org.jfree.util.ObjectUtilities;
85 import org.jfree.util.PaintUtilities;
86 import org.jfree.util.PublicCloneable;
87 import org.jfree.util.ShapeUtilities;
88 
89 /**
90  * A renderer that draws shapes for each data item, and lines between data
91  * items.  Each point has a mean value and a standard deviation line. For use
92  * with the {@link CategoryPlot} class.  The example shown
93  * here is generated by the <code>StatisticalLineChartDemo1.java</code> program
94  * included in the JFreeChart Demo Collection:
95  * <br><br>
96  * <img src="../../../../../images/StatisticalLineRendererSample.png"
97  * alt="StatisticalLineRendererSample.png" />
98  */
99 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      */
StatisticalLineAndShapeRenderer()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      */
StatisticalLineAndShapeRenderer(boolean linesVisible, boolean shapesVisible)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      */
getErrorIndicatorPaint()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      */
setErrorIndicatorPaint(Paint paint)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      */
getErrorIndicatorStroke()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      */
setErrorIndicatorStroke(Stroke stroke)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
findRangeBounds(CategoryDataset dataset)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
drawItem(Graphics2D g2, CategoryItemRendererState state, Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, ValueAxis rangeAxis, CategoryDataset dataset, int row, int column, int pass)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
equals(Object obj)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
hashCode()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      */
writeObject(ObjectOutputStream stream)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      */
readObject(ObjectInputStream stream)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 }
492