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