    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
    060    package org.jfree.chart.renderer.category;
    061
    062    import java.awt.BasicStroke;
    063    import java.awt.Color;
    064    import java.awt.Component;
    065    import java.awt.Graphics;
    066    import java.awt.Graphics2D;
    067    import java.awt.Paint;
    068    import java.awt.Shape;
    069    import java.awt.Stroke;
    070    import java.awt.geom.AffineTransform;
    071    import java.awt.geom.Arc2D;
    072    import java.awt.geom.GeneralPath;
    073    import java.awt.geom.Line2D;
    074    import java.awt.geom.Rectangle2D;
    075    import java.io.IOException;
    076    import java.io.ObjectInputStream;
    077    import java.io.ObjectOutputStream;
    078
    079    import javax.swing.Icon;
    080
    081    import org.jfree.chart.axis.CategoryAxis;
    082    import org.jfree.chart.axis.ValueAxis;
    083    import org.jfree.chart.entity.EntityCollection;
    084    import org.jfree.chart.event.RendererChangeEvent;
    085    import org.jfree.chart.plot.CategoryPlot;
    086    import org.jfree.chart.plot.PlotOrientation;
    087    import org.jfree.data.category.CategoryDataset;
    088    import org.jfree.io.SerialUtilities;
    089    import 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     */
    102    public 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    }