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