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