    001/* ===========================================================
    002 * JFreeChart : a free chart library for the Java(tm) platform
    003 * ===========================================================
    004 *
    005 * (C) Copyright 2000-2011, 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 * MinMaxCategoryRenderer.java
    029 * ---------------------------
    030 * (C) Copyright 2002-2008, by Object Refinery Limited.
    031 *
    032 * Original Author:  Tomer Peretz;
    033 * Contributor(s):   David Gilbert (for Object Refinery Limited);
    034 *                   Christian W. Zuckschwerdt;
    035 *                   Nicolas Brodu (for Astrium and EADS Corporate Research
    036 *                   Center);
    037 *
    038 * Changes:
    039 * --------
    040 * 29-May-2002 : Version 1 (TP);
    041 * 02-Oct-2002 : Fixed errors reported by Checkstyle (DG);
    042 * 24-Oct-2002 : Amendments for changes in CategoryDataset interface and
    043 *               CategoryToolTipGenerator interface (DG);
    044 * 05-Nov-2002 : Base dataset is now TableDataset not CategoryDataset (DG);
    045 * 17-Jan-2003 : Moved plot classes to a separate package (DG);
    046 * 10-Apr-2003 : Changed CategoryDataset to KeyedValues2DDataset in drawItem()
    047 *               method (DG);
    048 * 30-Jul-2003 : Modified entity constructor (CZ);
    049 * 08-Sep-2003 : Implemented Serializable (NB);
    050 * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG);
    051 * 05-Nov-2004 : Modified drawItem() signature (DG);
    052 * 17-Nov-2005 : Added change events and argument checks (DG);
    053 * ------------- JFREECHART 1.0.x ---------------------------------------------
    054 * 02-Feb-2007 : Removed author tags all over JFreeChart sources (DG);
    055 * 09-Mar-2007 : Fixed problem with horizontal rendering (DG);
    056 * 28-Sep-2007 : Added equals() method override (DG);
    057 *
    058 */
    059
    060package org.jfree.chart.renderer.category;
    061
    062import java.awt.BasicStroke;
    063import java.awt.Color;
    064import java.awt.Component;
    065import java.awt.Graphics;
    066import java.awt.Graphics2D;
    067import java.awt.Paint;
    068import java.awt.Shape;
    069import java.awt.Stroke;
    070import java.awt.geom.AffineTransform;
    071import java.awt.geom.Arc2D;
    072import java.awt.geom.GeneralPath;
    073import java.awt.geom.Line2D;
    074import java.awt.geom.Rectangle2D;
    075import java.io.IOException;
    076import java.io.ObjectInputStream;
    077import java.io.ObjectOutputStream;
    078
    079import javax.swing.Icon;
    080
    081import org.jfree.chart.axis.CategoryAxis;
    082import org.jfree.chart.axis.ValueAxis;
    083import org.jfree.chart.entity.EntityCollection;
    084import org.jfree.chart.event.RendererChangeEvent;
    085import org.jfree.chart.plot.CategoryPlot;
    086import org.jfree.chart.plot.PlotOrientation;
    087import org.jfree.data.category.CategoryDataset;
    088import org.jfree.io.SerialUtilities;
    089import org.jfree.util.PaintUtilities;
    090
    091/**
    092 * Renderer for drawing min max plot. This renderer draws all the series under
    093 * the same category in the same x position using <code>objectIcon</code> and
    094 * a line from the maximum value to the minimum value. For use with the
    095 * {@link CategoryPlot} class. The example shown here is generated by
    096 * the <code>MinMaxCategoryPlotDemo1.java</code> program included in the
    097 * JFreeChart Demo Collection:
    098 * <br><br>
    099 * <img src="../../../../../images/MinMaxCategoryRendererSample.png"
    100 * alt="MinMaxCategoryRendererSample.png" />
    101 */
    102public class MinMaxCategoryRenderer extends AbstractCategoryItemRenderer {
    103
    104    /** For serialization. */
    105    private static final long serialVersionUID = 2935615937671064911L;
    106
    107    /** A flag indicating whether or not lines are drawn between XY points. */
    108    private boolean plotLines = false;
    109
    110    /**
    111     * The paint of the line between the minimum value and the maximum value.
    112     */
    113    private transient Paint groupPaint = Color.black;
    114
    115    /**
    116     * The stroke of the line between the minimum value and the maximum value.
    117     */
    118    private transient Stroke groupStroke = new BasicStroke(1.0f);
    119
    120    /** The icon used to indicate the minimum value.*/
    121    private transient Icon minIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0,
    122            360, Arc2D.OPEN), null, Color.black);
    123
    124    /** The icon used to indicate the maximum value.*/
    125    private transient Icon maxIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0,
    126            360, Arc2D.OPEN), null, Color.black);
    127
    128    /** The icon used to indicate the values.*/
    129    private transient Icon objectIcon = getIcon(new Line2D.Double(-4, 0, 4, 0),
    130            false, true);
    131
    132    /** The last category. */
    133    private int lastCategory = -1;
    134
    135    /** The minimum. */
    136    private double min;
    137
    138    /** The maximum. */
    139    private double max;
    140
    141    /**
    142     * Default constructor.
    143     */
    144    public MinMaxCategoryRenderer() {
    145        super();
    146    }
    147
    148    /**
    149     * Gets whether or not lines are drawn between category points.
    150     *
    151     * @return boolean true if line will be drawn between sequenced categories,
    152     *         otherwise false.
    153     *
    154     * @see #setDrawLines(boolean)
    155     */
    156    public boolean isDrawLines() {
    157        return this.plotLines;
    158    }
    159
    160    /**
    161     * Sets the flag that controls whether or not lines are drawn to connect
    162     * the items within a series and sends a {@link RendererChangeEvent} to
    163     * all registered listeners.
    164     *
    165     * @param draw  the new value of the flag.
    166     *
    167     * @see #isDrawLines()
    168     */
    169    public void setDrawLines(boolean draw) {
    170        if (this.plotLines != draw) {
    171            this.plotLines = draw;
    172            fireChangeEvent();
    173        }
    174
    175    }
    176
    177    /**
    178     * Returns the paint used to draw the line between the minimum and maximum
    179     * value items in each category.
    180     *
    181     * @return The paint (never <code>null</code>).
    182     *
    183     * @see #setGroupPaint(Paint)
    184     */
    185    public Paint getGroupPaint() {
    186        return this.groupPaint;
    187    }
    188
    189    /**
    190     * Sets the paint used to draw the line between the minimum and maximum
    191     * value items in each category and sends a {@link RendererChangeEvent} to
    192     * all registered listeners.
    193     *
    194     * @param paint  the paint (<code>null</code> not permitted).
    195     *
    196     * @see #getGroupPaint()
    197     */
    198    public void setGroupPaint(Paint paint) {
    199        if (paint == null) {
    200            throw new IllegalArgumentException("Null 'paint' argument.");
    201        }
    202        this.groupPaint = paint;
    203        fireChangeEvent();
    204    }
    205
    206    /**
    207     * Returns the stroke used to draw the line between the minimum and maximum
    208     * value items in each category.
    209     *
    210     * @return The stroke (never <code>null</code>).
    211     *
    212     * @see #setGroupStroke(Stroke)
    213     */
    214    public Stroke getGroupStroke() {
    215        return this.groupStroke;
    216    }
    217
    218    /**
    219     * Sets the stroke of the line between the minimum value and the maximum
    220     * value and sends a {@link RendererChangeEvent} to all registered
    221     * listeners.
    222     *
    223     * @param stroke the new stroke (<code>null</code> not permitted).
    224     */
    225    public void setGroupStroke(Stroke stroke) {
    226        if (stroke == null) {
    227            throw new IllegalArgumentException("Null 'stroke' argument.");
    228        }
    229        this.groupStroke = stroke;
    230        fireChangeEvent();
    231    }
    232
    233    /**
    234     * Returns the icon drawn for each data item.
    235     *
    236     * @return The icon (never <code>null</code>).
    237     *
    238     * @see #setObjectIcon(Icon)
    239     */
    240    public Icon getObjectIcon() {
    241        return this.objectIcon;
    242    }
    243
    244    /**
    245     * Sets the icon drawn for each data item and sends a
    246     * {@link RendererChangeEvent} to all registered listeners.
    247     *
    248     * @param icon  the icon.
    249     *
    250     * @see #getObjectIcon()
    251     */
    252    public void setObjectIcon(Icon icon) {
    253        if (icon == null) {
    254            throw new IllegalArgumentException("Null 'icon' argument.");
    255        }
    256        this.objectIcon = icon;
    257        fireChangeEvent();
    258    }
    259
    260    /**
    261     * Returns the icon displayed for the maximum value data item within each
    262     * category.
    263     *
    264     * @return The icon (never <code>null</code>).
    265     *
    266     * @see #setMaxIcon(Icon)
    267     */
    268    public Icon getMaxIcon() {
    269        return this.maxIcon;
    270    }
    271
    272    /**
    273     * Sets the icon displayed for the maximum value data item within each
    274     * category and sends a {@link RendererChangeEvent} to all registered
    275     * listeners.
    276     *
    277     * @param icon  the icon (<code>null</code> not permitted).
    278     *
    279     * @see #getMaxIcon()
    280     */
    281    public void setMaxIcon(Icon icon) {
    282        if (icon == null) {
    283            throw new IllegalArgumentException("Null 'icon' argument.");
    284        }
    285        this.maxIcon = icon;
    286        fireChangeEvent();
    287    }
    288
    289    /**
    290     * Returns the icon displayed for the minimum value data item within each
    291     * category.
    292     *
    293     * @return The icon (never <code>null</code>).
    294     *
    295     * @see #setMinIcon(Icon)
    296     */
    297    public Icon getMinIcon() {
    298        return this.minIcon;
    299    }
    300
    301    /**
    302     * Sets the icon displayed for the minimum value data item within each
    303     * category and sends a {@link RendererChangeEvent} to all registered
    304     * listeners.
    305     *
    306     * @param icon  the icon (<code>null</code> not permitted).
    307     *
    308     * @see #getMinIcon()
    309     */
    310    public void setMinIcon(Icon icon) {
    311        if (icon == null) {
    312            throw new IllegalArgumentException("Null 'icon' argument.");
    313        }
    314        this.minIcon = icon;
    315        fireChangeEvent();
    316    }
    317
    318    /**
    319     * Draw a single data item.
    320     *
    321     * @param g2  the graphics device.
    322     * @param state  the renderer state.
    323     * @param dataArea  the area in which the data is drawn.
    324     * @param plot  the plot.
    325     * @param domainAxis  the domain axis.
    326     * @param rangeAxis  the range axis.
    327     * @param dataset  the dataset.
    328     * @param row  the row index (zero-based).
    329     * @param column  the column index (zero-based).
    330     * @param pass  the pass index.
    331     */
    332    public void drawItem(Graphics2D g2, CategoryItemRendererState state,
    333            Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis,
    334            ValueAxis rangeAxis, CategoryDataset dataset, int row, int column,
    335            int pass) {
    336
    337        // first check the number we are plotting...
    338        Number value = dataset.getValue(row, column);
    339        if (value != null) {
    340            // current data point...
    341            double x1 = domainAxis.getCategoryMiddle(column, getColumnCount(),
    342                    dataArea, plot.getDomainAxisEdge());
    343            double y1 = rangeAxis.valueToJava2D(value.doubleValue(), dataArea,
    344                    plot.getRangeAxisEdge());
    345            g2.setPaint(getItemPaint(row, column));
    346            g2.setStroke(getItemStroke(row, column));
    347            Shape shape = null;
    348            shape = new Rectangle2D.Double(x1 - 4, y1 - 4, 8.0, 8.0);
    349
    350            PlotOrientation orient = plot.getOrientation();
    351            if (orient == PlotOrientation.VERTICAL) {
    352                this.objectIcon.paintIcon(null, g2, (int) x1, (int) y1);
    353            }
    354            else {
    355                this.objectIcon.paintIcon(null, g2, (int) y1, (int) x1);
    356            }
    357
    358            if (this.lastCategory == column) {
    359                if (this.min > value.doubleValue()) {
    360                    this.min = value.doubleValue();
    361                }
    362                if (this.max < value.doubleValue()) {
    363                    this.max = value.doubleValue();
    364                }
    365
    366                // last series, so we are ready to draw the min and max
    367                if (dataset.getRowCount() - 1 == row) {
    368                    g2.setPaint(this.groupPaint);
    369                    g2.setStroke(this.groupStroke);
    370                    double minY = rangeAxis.valueToJava2D(this.min, dataArea,
    371                            plot.getRangeAxisEdge());
    372                    double maxY = rangeAxis.valueToJava2D(this.max, dataArea,
    373                            plot.getRangeAxisEdge());
    374
    375                    if (orient == PlotOrientation.VERTICAL) {
    376                        g2.draw(new Line2D.Double(x1, minY, x1, maxY));
    377                        this.minIcon.paintIcon(null, g2, (int) x1, (int) minY);
    378                        this.maxIcon.paintIcon(null, g2, (int) x1, (int) maxY);
    379                    }
    380                    else {
    381                        g2.draw(new Line2D.Double(minY, x1, maxY, x1));
    382                        this.minIcon.paintIcon(null, g2, (int) minY, (int) x1);
    383                        this.maxIcon.paintIcon(null, g2, (int) maxY, (int) x1);
    384                    }
    385                }
    386            }
    387            else {  // reset the min and max
    388                this.lastCategory = column;
    389                this.min = value.doubleValue();
    390                this.max = value.doubleValue();
    391            }
    392
    393            // connect to the previous point
    394            if (this.plotLines) {
    395                if (column != 0) {
    396                    Number previousValue = dataset.getValue(row, column - 1);
    397                    if (previousValue != null) {
    398                        // previous data point...
    399                        double previous = previousValue.doubleValue();
    400                        double x0 = domainAxis.getCategoryMiddle(column - 1,
    401                                getColumnCount(), dataArea,
    402                                plot.getDomainAxisEdge());
    403                        double y0 = rangeAxis.valueToJava2D(previous, dataArea,
    404                                plot.getRangeAxisEdge());
    405                        g2.setPaint(getItemPaint(row, column));
    406                        g2.setStroke(getItemStroke(row, column));
    407                        Line2D line;
    408                        if (orient == PlotOrientation.VERTICAL) {
    409                            line = new Line2D.Double(x0, y0, x1, y1);
    410                        }
    411                        else {
    412                            line = new Line2D.Double(y0, x0, y1, x1);
    413                        }
    414                        g2.draw(line);
    415                    }
    416                }
    417            }
    418
    419            // add an item entity, if this information is being collected
    420            EntityCollection entities = state.getEntityCollection();
    421            if (entities != null && shape != null) {
    422                addItemEntity(entities, dataset, row, column, shape);
    423            }
    424        }
    425    }
    426
    427    /**
    428     * Tests this instance for equality with an arbitrary object.  The icon
    429     * fields are NOT included in the test, so this implementation is a little
    430     * weak.
    431     *
    432     * @param obj  the object (<code>null</code> permitted).
    433     *
    434     * @return A boolean.
    435     *
    436     * @since 1.0.7
    437     */
    438    public boolean equals(Object obj) {
    439        if (obj == this) {
    440            return true;
    441        }
    442        if (!(obj instanceof MinMaxCategoryRenderer)) {
    443            return false;
    444        }
    445        MinMaxCategoryRenderer that = (MinMaxCategoryRenderer) obj;
    446        if (this.plotLines != that.plotLines) {
    447            return false;
    448        }
    449        if (!PaintUtilities.equal(this.groupPaint, that.groupPaint)) {
    450            return false;
    451        }
    452        if (!this.groupStroke.equals(that.groupStroke)) {
    453            return false;
    454        }
    455        return super.equals(obj);
    456    }
    457
    458    /**
    459     * Returns an icon.
    460     *
    461     * @param shape  the shape.
    462     * @param fillPaint  the fill paint.
    463     * @param outlinePaint  the outline paint.
    464     *
    465     * @return The icon.
    466     */
    467    private Icon getIcon(Shape shape, final Paint fillPaint,
    468                        final Paint outlinePaint) {
    469
    470      final int width = shape.getBounds().width;
    471      final int height = shape.getBounds().height;
    472      final GeneralPath path = new GeneralPath(shape);
    473      return new Icon() {
    474          public void paintIcon(Component c, Graphics g, int x, int y) {
    475              Graphics2D g2 = (Graphics2D) g;
    476              path.transform(AffineTransform.getTranslateInstance(x, y));
    477              if (fillPaint != null) {
    478                  g2.setPaint(fillPaint);
    479                  g2.fill(path);
    480              }
    481              if (outlinePaint != null) {
    482                  g2.setPaint(outlinePaint);
    483                  g2.draw(path);
    484              }
    485              path.transform(AffineTransform.getTranslateInstance(-x, -y));
    486        }
    487
    488        public int getIconWidth() {
    489            return width;
    490        }
    491
    492        public int getIconHeight() {
    493            return height;
    494        }
    495
    496      };
    497    }
    498
    499    /**
    500     * Returns an icon from a shape.
    501     *
    502     * @param shape  the shape.
    503     * @param fill  the fill flag.
    504     * @param outline  the outline flag.
    505     *
    506     * @return The icon.
    507     */
    508    private Icon getIcon(Shape shape, final boolean fill,
    509            final boolean outline) {
    510        final int width = shape.getBounds().width;
    511        final int height = shape.getBounds().height;
    512        final GeneralPath path = new GeneralPath(shape);
    513        return new Icon() {
    514            public void paintIcon(Component c, Graphics g, int x, int y) {
    515                Graphics2D g2 = (Graphics2D) g;
    516                path.transform(AffineTransform.getTranslateInstance(x, y));
    517                if (fill) {
    518                    g2.fill(path);
    519                }
    520                if (outline) {
    521                    g2.draw(path);
    522                }
    523                path.transform(AffineTransform.getTranslateInstance(-x, -y));
    524            }
    525
    526            public int getIconWidth() {
    527                return width;
    528            }
    529
    530            public int getIconHeight() {
    531                return height;
    532            }
    533        };
    534    }
    535
    536    /**
    537     * Provides serialization support.
    538     *
    539     * @param stream  the output stream.
    540     *
    541     * @throws IOException  if there is an I/O error.
    542     */
    543    private void writeObject(ObjectOutputStream stream) throws IOException {
    544        stream.defaultWriteObject();
    545        SerialUtilities.writeStroke(this.groupStroke, stream);
    546        SerialUtilities.writePaint(this.groupPaint, stream);
    547    }
    548
    549    /**
    550     * Provides serialization support.
    551     *
    552     * @param stream  the input stream.
    553     *
    554     * @throws IOException  if there is an I/O error.
    555     * @throws ClassNotFoundException  if there is a classpath problem.
    556     */
    557    private void readObject(ObjectInputStream stream)
    558        throws IOException, ClassNotFoundException {
    559        stream.defaultReadObject();
    560        this.groupStroke = SerialUtilities.readStroke(stream);
    561        this.groupPaint = SerialUtilities.readPaint(stream);
    562
    563        this.minIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0, 360,
    564                Arc2D.OPEN), null, Color.black);
    565        this.maxIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0, 360,
    566                Arc2D.OPEN), null, Color.black);
    567        this.objectIcon = getIcon(new Line2D.Double(-4, 0, 4, 0), false, true);
    568    }
    569
    570}