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 * StatisticalBarRenderer.java
029 * ---------------------------
030 * (C) Copyright 2002-present, by Pascal Collet and Contributors.
031 *
032 * Original Author:  Pascal Collet;
033 * Contributor(s):   David Gilbert;
034 *                   Christian W. Zuckschwerdt;
035 *                   Peter Kolb (patches 2497611, 2791407);
036 *                   Martin Hoeller;
037 *
038 */
039
040package org.jfree.chart.renderer.category;
041
042import java.awt.BasicStroke;
043import java.awt.Color;
044import java.awt.GradientPaint;
045import java.awt.Graphics2D;
046import java.awt.Paint;
047import java.awt.Stroke;
048import java.awt.geom.Line2D;
049import java.awt.geom.Rectangle2D;
050import java.io.IOException;
051import java.io.ObjectInputStream;
052import java.io.ObjectOutputStream;
053import java.io.Serializable;
054import java.util.Objects;
055
056import org.jfree.chart.axis.CategoryAxis;
057import org.jfree.chart.axis.ValueAxis;
058import org.jfree.chart.entity.EntityCollection;
059import org.jfree.chart.event.RendererChangeEvent;
060import org.jfree.chart.labels.CategoryItemLabelGenerator;
061import org.jfree.chart.plot.CategoryPlot;
062import org.jfree.chart.plot.PlotOrientation;
063import org.jfree.chart.ui.GradientPaintTransformer;
064import org.jfree.chart.ui.RectangleEdge;
065import org.jfree.chart.util.PaintUtils;
066import org.jfree.chart.util.PublicCloneable;
067import org.jfree.chart.util.SerialUtils;
068import org.jfree.data.Range;
069import org.jfree.data.category.CategoryDataset;
070import org.jfree.data.statistics.StatisticalCategoryDataset;
071
072/**
073 * A renderer that handles the drawing a bar plot where
074 * each bar has a mean value and a standard deviation line.  The example shown
075 * here is generated by the {@code StatisticalBarChartDemo1.java} program
076 * included in the JFreeChart Demo Collection:
077 * <br><br>
078 * <img src="doc-files/StatisticalBarRendererSample.png"
079 * alt="StatisticalBarRendererSample.png">
080 */
081public class StatisticalBarRenderer extends BarRenderer
082        implements CategoryItemRenderer, Cloneable, PublicCloneable,
083                   Serializable {
084
085    /** For serialization. */
086    private static final long serialVersionUID = -4986038395414039117L;
087
088    /** The paint used to show the error indicator. */
089    private transient Paint errorIndicatorPaint;
090
091    /**
092     * The stroke used to draw the error indicators.
093     */
094    private transient Stroke errorIndicatorStroke;
095
096    /**
097     * Default constructor.
098     */
099    public StatisticalBarRenderer() {
100        super();
101        this.errorIndicatorPaint = Color.GRAY;
102        this.errorIndicatorStroke = new BasicStroke(1.0f);
103    }
104
105    /**
106     * Returns the paint used for the error indicators.
107     *
108     * @return The paint used for the error indicators (possibly
109     *         {@code null}).
110     *
111     * @see #setErrorIndicatorPaint(Paint)
112     */
113    public Paint getErrorIndicatorPaint() {
114        return this.errorIndicatorPaint;
115    }
116
117    /**
118     * Sets the paint used for the error indicators (if {@code null},
119     * the item outline paint is used instead) and sends a
120     * {@link RendererChangeEvent} to all registered listeners.
121     *
122     * @param paint  the paint ({@code null} permitted).
123     *
124     * @see #getErrorIndicatorPaint()
125     */
126    public void setErrorIndicatorPaint(Paint paint) {
127        this.errorIndicatorPaint = paint;
128        fireChangeEvent();
129    }
130
131    /**
132     * Returns the stroke used to draw the error indicators.  If this is
133     * {@code null}, the renderer will use the item outline stroke).
134     *
135     * @return The stroke (possibly {@code null}).
136     *
137     * @see #setErrorIndicatorStroke(Stroke)
138     */
139    public Stroke getErrorIndicatorStroke() {
140        return this.errorIndicatorStroke;
141    }
142
143    /**
144     * Sets the stroke used to draw the error indicators, and sends a
145     * {@link RendererChangeEvent} to all registered listeners.  If you set
146     * this to {@code null}, the renderer will use the item outline
147     * stroke.
148     *
149     * @param stroke  the stroke ({@code null} permitted).
150     *
151     * @see #getErrorIndicatorStroke()
152     */
153    public void setErrorIndicatorStroke(Stroke stroke) {
154        this.errorIndicatorStroke = stroke;
155        fireChangeEvent();
156    }
157
158    /**
159     * Returns the range of values the renderer requires to display all the
160     * items from the specified dataset. This takes into account the range
161     * between the min/max values, possibly ignoring invisible series.
162     *
163     * @param dataset  the dataset ({@code null} permitted).
164     *
165     * @return The range (or {@code null} if the dataset is
166     *         {@code null} or empty).
167     */
168    @Override
169    public Range findRangeBounds(CategoryDataset dataset) {
170         return findRangeBounds(dataset, true);
171    }
172
173    /**
174     * Draws the bar with its standard deviation line range for a single
175     * (series, category) data item.
176     *
177     * @param g2  the graphics device.
178     * @param state  the renderer state.
179     * @param dataArea  the data area.
180     * @param plot  the plot.
181     * @param domainAxis  the domain axis.
182     * @param rangeAxis  the range axis.
183     * @param data  the data.
184     * @param row  the row index (zero-based).
185     * @param column  the column index (zero-based).
186     * @param pass  the pass index.
187     */
188    @Override
189    public void drawItem(Graphics2D g2, CategoryItemRendererState state,
190            Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis,
191            ValueAxis rangeAxis, CategoryDataset data, int row, int column,
192            int pass) {
193
194        int visibleRow = state.getVisibleSeriesIndex(row);
195        if (visibleRow < 0) {
196            return;
197        }
198        // defensive check
199        if (!(data instanceof StatisticalCategoryDataset)) {
200            throw new IllegalArgumentException(
201                "Requires StatisticalCategoryDataset.");
202        }
203        StatisticalCategoryDataset statData = (StatisticalCategoryDataset) data;
204
205        PlotOrientation orientation = plot.getOrientation();
206        if (orientation == PlotOrientation.HORIZONTAL) {
207            drawHorizontalItem(g2, state, dataArea, plot, domainAxis,
208                    rangeAxis, statData, visibleRow, row, column);
209        }
210        else if (orientation == PlotOrientation.VERTICAL) {
211            drawVerticalItem(g2, state, dataArea, plot, domainAxis, rangeAxis,
212                    statData, visibleRow, row, column);
213        }
214    }
215
216    /**
217     * Draws an item for a plot with a horizontal orientation.
218     *
219     * @param g2  the graphics device.
220     * @param state  the renderer state.
221     * @param dataArea  the data area.
222     * @param plot  the plot.
223     * @param domainAxis  the domain axis.
224     * @param rangeAxis  the range axis.
225     * @param dataset  the data.
226     * @param visibleRow  the visible row index.
227     * @param row  the row index (zero-based).
228     * @param column  the column index (zero-based).
229     */
230    protected void drawHorizontalItem(Graphics2D g2,
231                                      CategoryItemRendererState state,
232                                      Rectangle2D dataArea,
233                                      CategoryPlot plot,
234                                      CategoryAxis domainAxis,
235                                      ValueAxis rangeAxis,
236                                      StatisticalCategoryDataset dataset,
237                                      int visibleRow,
238                                      int row,
239                                      int column) {
240
241        // BAR Y
242        double rectY = calculateBarW0(plot, PlotOrientation.HORIZONTAL, 
243                dataArea, domainAxis, state, visibleRow, column);
244
245        // BAR X
246        Number meanValue = dataset.getMeanValue(row, column);
247        if (meanValue == null) {
248            return;
249        }
250        double value = meanValue.doubleValue();
251        double base = 0.0;
252        double lclip = getLowerClip();
253        double uclip = getUpperClip();
254
255        if (uclip <= 0.0) {  // cases 1, 2, 3 and 4
256            if (value >= uclip) {
257                return; // bar is not visible
258            }
259            base = uclip;
260            if (value <= lclip) {
261                value = lclip;
262            }
263        }
264        else if (lclip <= 0.0) { // cases 5, 6, 7 and 8
265            if (value >= uclip) {
266                value = uclip;
267            }
268            else {
269                if (value <= lclip) {
270                    value = lclip;
271                }
272            }
273        }
274        else { // cases 9, 10, 11 and 12
275            if (value <= lclip) {
276                return; // bar is not visible
277            }
278            base = getLowerClip();
279            if (value >= uclip) {
280               value = uclip;
281            }
282        }
283
284        RectangleEdge yAxisLocation = plot.getRangeAxisEdge();
285        double transY1 = rangeAxis.valueToJava2D(base, dataArea, yAxisLocation);
286        double transY2 = rangeAxis.valueToJava2D(value, dataArea,
287                yAxisLocation);
288        double rectX = Math.min(transY2, transY1);
289
290        double rectHeight = state.getBarWidth();
291        double rectWidth = Math.abs(transY2 - transY1);
292
293        Rectangle2D bar = new Rectangle2D.Double(rectX, rectY, rectWidth,
294                rectHeight);
295        Paint itemPaint = getItemPaint(row, column);
296        GradientPaintTransformer t = getGradientPaintTransformer();
297        if (t != null && itemPaint instanceof GradientPaint) {
298            itemPaint = t.transform((GradientPaint) itemPaint, bar);
299        }
300        g2.setPaint(itemPaint);
301        g2.fill(bar);
302
303        // draw the outline...
304        if (isDrawBarOutline()
305                && state.getBarWidth() > BAR_OUTLINE_WIDTH_THRESHOLD) {
306            Stroke stroke = getItemOutlineStroke(row, column);
307            Paint paint = getItemOutlinePaint(row, column);
308            if (stroke != null && paint != null) {
309                g2.setStroke(stroke);
310                g2.setPaint(paint);
311                g2.draw(bar);
312            }
313        }
314
315        // standard deviation lines
316        Number n = dataset.getStdDevValue(row, column);
317        if (n != null) {
318            double valueDelta = n.doubleValue();
319            double highVal = rangeAxis.valueToJava2D(meanValue.doubleValue()
320                    + valueDelta, dataArea, yAxisLocation);
321            double lowVal = rangeAxis.valueToJava2D(meanValue.doubleValue()
322                    - valueDelta, dataArea, yAxisLocation);
323
324            if (this.errorIndicatorPaint != null) {
325                g2.setPaint(this.errorIndicatorPaint);
326            }
327            else {
328                g2.setPaint(getItemOutlinePaint(row, column));
329            }
330            if (this.errorIndicatorStroke != null) {
331                g2.setStroke(this.errorIndicatorStroke);
332            }
333            else {
334                g2.setStroke(getItemOutlineStroke(row, column));
335            }
336            Line2D line;
337            line = new Line2D.Double(lowVal, rectY + rectHeight / 2.0d,
338                                     highVal, rectY + rectHeight / 2.0d);
339            g2.draw(line);
340            line = new Line2D.Double(highVal, rectY + rectHeight * 0.25,
341                                     highVal, rectY + rectHeight * 0.75);
342            g2.draw(line);
343            line = new Line2D.Double(lowVal, rectY + rectHeight * 0.25,
344                                     lowVal, rectY + rectHeight * 0.75);
345            g2.draw(line);
346        }
347
348        CategoryItemLabelGenerator generator = getItemLabelGenerator(row,
349                column);
350        if (generator != null && isItemLabelVisible(row, column)) {
351            drawItemLabel(g2, dataset, row, column, plot, generator, bar,
352                    (value < 0.0));
353        }
354
355        // add an item entity, if this information is being collected
356        EntityCollection entities = state.getEntityCollection();
357        if (entities != null) {
358            addItemEntity(entities, dataset, row, column, bar);
359        }
360
361    }
362
363    /**
364     * Draws an item for a plot with a vertical orientation.
365     *
366     * @param g2  the graphics device.
367     * @param state  the renderer state.
368     * @param dataArea  the data area.
369     * @param plot  the plot.
370     * @param domainAxis  the domain axis.
371     * @param rangeAxis  the range axis.
372     * @param dataset  the data.
373     * @param visibleRow  the visible row index.
374     * @param row  the row index (zero-based).
375     * @param column  the column index (zero-based).
376     */
377    protected void drawVerticalItem(Graphics2D g2,
378                                    CategoryItemRendererState state,
379                                    Rectangle2D dataArea,
380                                    CategoryPlot plot,
381                                    CategoryAxis domainAxis,
382                                    ValueAxis rangeAxis,
383                                    StatisticalCategoryDataset dataset,
384                                    int visibleRow,
385                                    int row,
386                                    int column) {
387
388        // BAR X
389        double rectX = calculateBarW0(plot, PlotOrientation.VERTICAL, dataArea,
390                domainAxis, state, visibleRow, column);
391
392        // BAR Y
393        Number meanValue = dataset.getMeanValue(row, column);
394        if (meanValue == null) {
395            return;
396        }
397
398        double value = meanValue.doubleValue();
399        double base = 0.0;
400        double lclip = getLowerClip();
401        double uclip = getUpperClip();
402
403        if (uclip <= 0.0) {  // cases 1, 2, 3 and 4
404            if (value >= uclip) {
405                return; // bar is not visible
406            }
407            base = uclip;
408            if (value <= lclip) {
409                value = lclip;
410            }
411        }
412        else if (lclip <= 0.0) { // cases 5, 6, 7 and 8
413            if (value >= uclip) {
414                value = uclip;
415            }
416            else {
417                if (value <= lclip) {
418                    value = lclip;
419                }
420            }
421        }
422        else { // cases 9, 10, 11 and 12
423            if (value <= lclip) {
424                return; // bar is not visible
425            }
426            base = getLowerClip();
427            if (value >= uclip) {
428               value = uclip;
429            }
430        }
431
432        RectangleEdge yAxisLocation = plot.getRangeAxisEdge();
433        double transY1 = rangeAxis.valueToJava2D(base, dataArea, yAxisLocation);
434        double transY2 = rangeAxis.valueToJava2D(value, dataArea,
435                yAxisLocation);
436        double rectY = Math.min(transY2, transY1);
437
438        double rectWidth = state.getBarWidth();
439        double rectHeight = Math.abs(transY2 - transY1);
440
441        Rectangle2D bar = new Rectangle2D.Double(rectX, rectY, rectWidth,
442                rectHeight);
443        Paint itemPaint = getItemPaint(row, column);
444        GradientPaintTransformer t = getGradientPaintTransformer();
445        if (t != null && itemPaint instanceof GradientPaint) {
446            itemPaint = t.transform((GradientPaint) itemPaint, bar);
447        }
448        g2.setPaint(itemPaint);
449        g2.fill(bar);
450        // draw the outline...
451        if (isDrawBarOutline()
452                && state.getBarWidth() > BAR_OUTLINE_WIDTH_THRESHOLD) {
453            Stroke stroke = getItemOutlineStroke(row, column);
454            Paint paint = getItemOutlinePaint(row, column);
455            if (stroke != null && paint != null) {
456                g2.setStroke(stroke);
457                g2.setPaint(paint);
458                g2.draw(bar);
459            }
460        }
461
462        // standard deviation lines
463        Number n = dataset.getStdDevValue(row, column);
464        if (n != null) {
465            double valueDelta = n.doubleValue();
466            double highVal = rangeAxis.valueToJava2D(meanValue.doubleValue()
467                    + valueDelta, dataArea, yAxisLocation);
468            double lowVal = rangeAxis.valueToJava2D(meanValue.doubleValue()
469                    - valueDelta, dataArea, yAxisLocation);
470
471            if (this.errorIndicatorPaint != null) {
472                g2.setPaint(this.errorIndicatorPaint);
473            }
474            else {
475                g2.setPaint(getItemOutlinePaint(row, column));
476            }
477            if (this.errorIndicatorStroke != null) {
478                g2.setStroke(this.errorIndicatorStroke);
479            }
480            else {
481                g2.setStroke(getItemOutlineStroke(row, column));
482            }
483
484            Line2D line;
485            line = new Line2D.Double(rectX + rectWidth / 2.0d, lowVal,
486                                     rectX + rectWidth / 2.0d, highVal);
487            g2.draw(line);
488            line = new Line2D.Double(rectX + rectWidth / 2.0d - 5.0d, highVal,
489                                     rectX + rectWidth / 2.0d + 5.0d, highVal);
490            g2.draw(line);
491            line = new Line2D.Double(rectX + rectWidth / 2.0d - 5.0d, lowVal,
492                                     rectX + rectWidth / 2.0d + 5.0d, lowVal);
493            g2.draw(line);
494        }
495
496        CategoryItemLabelGenerator generator = getItemLabelGenerator(row,
497                column);
498        if (generator != null && isItemLabelVisible(row, column)) {
499            drawItemLabel(g2, dataset, row, column, plot, generator, bar,
500                    (value < 0.0));
501        }
502
503        // add an item entity, if this information is being collected
504        EntityCollection entities = state.getEntityCollection();
505        if (entities != null) {
506            addItemEntity(entities, dataset, row, column, bar);
507        }
508    }
509
510    /**
511     * Tests this renderer for equality with an arbitrary object.
512     *
513     * @param obj  the object ({@code null} permitted).
514     *
515     * @return A boolean.
516     */
517    @Override
518    public boolean equals(Object obj) {
519        if (obj == this) {
520            return true;
521        }
522        if (!(obj instanceof StatisticalBarRenderer)) {
523            return false;
524        }
525        StatisticalBarRenderer that = (StatisticalBarRenderer) obj;
526        if (!PaintUtils.equal(this.errorIndicatorPaint,
527                that.errorIndicatorPaint)) {
528            return false;
529        }
530        if (!Objects.equals(this.errorIndicatorStroke,
531                that.errorIndicatorStroke)) {
532            return false;
533        }
534        return super.equals(obj);
535    }
536
537    /**
538     * Provides serialization support.
539     *
540     * @param stream  the output stream.
541     *
542     * @throws IOException  if there is an I/O error.
543     */
544    private void writeObject(ObjectOutputStream stream) throws IOException {
545        stream.defaultWriteObject();
546        SerialUtils.writePaint(this.errorIndicatorPaint, stream);
547        SerialUtils.writeStroke(this.errorIndicatorStroke, stream);
548    }
549
550    /**
551     * Provides serialization support.
552     *
553     * @param stream  the input stream.
554     *
555     * @throws IOException  if there is an I/O error.
556     * @throws ClassNotFoundException  if there is a classpath problem.
557     */
558    private void readObject(ObjectInputStream stream)
559        throws IOException, ClassNotFoundException {
560        stream.defaultReadObject();
561        this.errorIndicatorPaint = SerialUtils.readPaint(stream);
562        this.errorIndicatorStroke = SerialUtils.readStroke(stream);
563    }
564
565}