    001    /* ===========================================================
    002     * JFreeChart : a free chart library for the Java(tm) platform
    003     * ===========================================================
    004     *
    005     * (C) Copyright 2000-2008, 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     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
    025     * in the United States and other countries.]
    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.
    095     * <p>
    096     * For use with the {@link org.jfree.chart.plot.CategoryPlot} class.
    097     */
    098    public class MinMaxCategoryRenderer extends AbstractCategoryItemRenderer {
    099
    100        /** For serialization. */
    101        private static final long serialVersionUID = 2935615937671064911L;
    102
    103        /** A flag indicating whether or not lines are drawn between XY points. */
    104        private boolean plotLines = false;
    105
    106        /**
    107         * The paint of the line between the minimum value and the maximum value.
    108         */
    109        private transient Paint groupPaint = Color.black;
    110
    111        /**
    112         * The stroke of the line between the minimum value and the maximum value.
    113         */
    114        private transient Stroke groupStroke = new BasicStroke(1.0f);
    115
    116        /** The icon used to indicate the minimum value.*/
    117        private transient Icon minIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0,
    118                360, Arc2D.OPEN), null, Color.black);
    119
    120        /** The icon used to indicate the maximum value.*/
    121        private transient Icon maxIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0,
    122                360, Arc2D.OPEN), null, Color.black);
    123
    124        /** The icon used to indicate the values.*/
    125        private transient Icon objectIcon = getIcon(new Line2D.Double(-4, 0, 4, 0),
    126                false, true);
    127
    128        /** The last category. */
    129        private int lastCategory = -1;
    130
    131        /** The minimum. */
    132        private double min;
    133
    134        /** The maximum. */
    135        private double max;
    136
    137        /**
    138         * Default constructor.
    139         */
    140        public MinMaxCategoryRenderer() {
    141            super();
    142        }
    143
    144        /**
    145         * Gets whether or not lines are drawn between category points.
    146         *
    147         * @return boolean true if line will be drawn between sequenced categories,
    148         *         otherwise false.
    149         *
    150         * @see #setDrawLines(boolean)
    151         */
    152        public boolean isDrawLines() {
    153            return this.plotLines;
    154        }
    155
    156        /**
    157         * Sets the flag that controls whether or not lines are drawn to connect
    158         * the items within a series and sends a {@link RendererChangeEvent} to
    159         * all registered listeners.
    160         *
    161         * @param draw  the new value of the flag.
    162         *
    163         * @see #isDrawLines()
    164         */
    165        public void setDrawLines(boolean draw) {
    166            if (this.plotLines != draw) {
    167                this.plotLines = draw;
    168                fireChangeEvent();
    169            }
    170
    171        }
    172
    173        /**
    174         * Returns the paint used to draw the line between the minimum and maximum
    175         * value items in each category.
    176         *
    177         * @return The paint (never <code>null</code>).
    178         *
    179         * @see #setGroupPaint(Paint)
    180         */
    181        public Paint getGroupPaint() {
    182            return this.groupPaint;
    183        }
    184
    185        /**
    186         * Sets the paint used to draw the line between the minimum and maximum
    187         * value items in each category and sends a {@link RendererChangeEvent} to
    188         * all registered listeners.
    189         *
    190         * @param paint  the paint (<code>null</code> not permitted).
    191         *
    192         * @see #getGroupPaint()
    193         */
    194        public void setGroupPaint(Paint paint) {
    195            if (paint == null) {
    196                throw new IllegalArgumentException("Null 'paint' argument.");
    197            }
    198            this.groupPaint = paint;
    199            fireChangeEvent();
    200        }
    201
    202        /**
    203         * Returns the stroke used to draw the line between the minimum and maximum
    204         * value items in each category.
    205         *
    206         * @return The stroke (never <code>null</code>).
    207         *
    208         * @see #setGroupStroke(Stroke)
    209         */
    210        public Stroke getGroupStroke() {
    211            return this.groupStroke;
    212        }
    213
    214        /**
    215         * Sets the stroke of the line between the minimum value and the maximum
    216         * value and sends a {@link RendererChangeEvent} to all registered
    217         * listeners.
    218         *
    219         * @param stroke the new stroke (<code>null</code> not permitted).
    220         */
    221        public void setGroupStroke(Stroke stroke) {
    222            if (stroke == null) {
    223                throw new IllegalArgumentException("Null 'stroke' argument.");
    224            }
    225            this.groupStroke = stroke;
    226            fireChangeEvent();
    227        }
    228
    229        /**
    230         * Returns the icon drawn for each data item.
    231         *
    232         * @return The icon (never <code>null</code>).
    233         *
    234         * @see #setObjectIcon(Icon)
    235         */
    236        public Icon getObjectIcon() {
    237            return this.objectIcon;
    238        }
    239
    240        /**
    241         * Sets the icon drawn for each data item and sends a
    242         * {@link RendererChangeEvent} to all registered listeners.
    243         *
    244         * @param icon  the icon.
    245         *
    246         * @see #getObjectIcon()
    247         */
    248        public void setObjectIcon(Icon icon) {
    249            if (icon == null) {
    250                throw new IllegalArgumentException("Null 'icon' argument.");
    251            }
    252            this.objectIcon = icon;
    253            fireChangeEvent();
    254        }
    255
    256        /**
    257         * Returns the icon displayed for the maximum value data item within each
    258         * category.
    259         *
    260         * @return The icon (never <code>null</code>).
    261         *
    262         * @see #setMaxIcon(Icon)
    263         */
    264        public Icon getMaxIcon() {
    265            return this.maxIcon;
    266        }
    267
    268        /**
    269         * Sets the icon displayed for the maximum value data item within each
    270         * category and sends a {@link RendererChangeEvent} to all registered
    271         * listeners.
    272         *
    273         * @param icon  the icon (<code>null</code> not permitted).
    274         *
    275         * @see #getMaxIcon()
    276         */
    277        public void setMaxIcon(Icon icon) {
    278            if (icon == null) {
    279                throw new IllegalArgumentException("Null 'icon' argument.");
    280            }
    281            this.maxIcon = icon;
    282            fireChangeEvent();
    283        }
    284
    285        /**
    286         * Returns the icon displayed for the minimum value data item within each
    287         * category.
    288         *
    289         * @return The icon (never <code>null</code>).
    290         *
    291         * @see #setMinIcon(Icon)
    292         */
    293        public Icon getMinIcon() {
    294            return this.minIcon;
    295        }
    296
    297        /**
    298         * Sets the icon displayed for the minimum value data item within each
    299         * category and sends a {@link RendererChangeEvent} to all registered
    300         * listeners.
    301         *
    302         * @param icon  the icon (<code>null</code> not permitted).
    303         *
    304         * @see #getMinIcon()
    305         */
    306        public void setMinIcon(Icon icon) {
    307            if (icon == null) {
    308                throw new IllegalArgumentException("Null 'icon' argument.");
    309            }
    310            this.minIcon = icon;
    311            fireChangeEvent();
    312        }
    313
    314        /**
    315         * Draw a single data item.
    316         *
    317         * @param g2  the graphics device.
    318         * @param state  the renderer state.
    319         * @param dataArea  the area in which the data is drawn.
    320         * @param plot  the plot.
    321         * @param domainAxis  the domain axis.
    322         * @param rangeAxis  the range axis.
    323         * @param dataset  the dataset.
    324         * @param row  the row index (zero-based).
    325         * @param column  the column index (zero-based).
    326         * @param pass  the pass index.
    327         */
    328        public void drawItem(Graphics2D g2, CategoryItemRendererState state,
    329                Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis,
    330                ValueAxis rangeAxis, CategoryDataset dataset, int row, int column,
    331                int pass) {
    332
    333            // first check the number we are plotting...
    334            Number value = dataset.getValue(row, column);
    335            if (value != null) {
    336                // current data point...
    337                double x1 = domainAxis.getCategoryMiddle(column, getColumnCount(),
    338                        dataArea, plot.getDomainAxisEdge());
    339                double y1 = rangeAxis.valueToJava2D(value.doubleValue(), dataArea,
    340                        plot.getRangeAxisEdge());
    341                g2.setPaint(getItemPaint(row, column));
    342                g2.setStroke(getItemStroke(row, column));
    343                Shape shape = null;
    344                shape = new Rectangle2D.Double(x1 - 4, y1 - 4, 8.0, 8.0);
    345
    346                PlotOrientation orient = plot.getOrientation();
    347                if (orient == PlotOrientation.VERTICAL) {
    348                    this.objectIcon.paintIcon(null, g2, (int) x1, (int) y1);
    349                }
    350                else {
    351                    this.objectIcon.paintIcon(null, g2, (int) y1, (int) x1);
    352                }
    353
    354                if (this.lastCategory == column) {
    355                    if (this.min > value.doubleValue()) {
    356                        this.min = value.doubleValue();
    357                    }
    358                    if (this.max < value.doubleValue()) {
    359                        this.max = value.doubleValue();
    360                    }
    361
    362                    // last series, so we are ready to draw the min and max
    363                    if (dataset.getRowCount() - 1 == row) {
    364                        g2.setPaint(this.groupPaint);
    365                        g2.setStroke(this.groupStroke);
    366                        double minY = rangeAxis.valueToJava2D(this.min, dataArea,
    367                                plot.getRangeAxisEdge());
    368                        double maxY = rangeAxis.valueToJava2D(this.max, dataArea,
    369                                plot.getRangeAxisEdge());
    370
    371                        if (orient == PlotOrientation.VERTICAL) {
    372                            g2.draw(new Line2D.Double(x1, minY, x1, maxY));
    373                            this.minIcon.paintIcon(null, g2, (int) x1, (int) minY);
    374                            this.maxIcon.paintIcon(null, g2, (int) x1, (int) maxY);
    375                        }
    376                        else {
    377                            g2.draw(new Line2D.Double(minY, x1, maxY, x1));
    378                            this.minIcon.paintIcon(null, g2, (int) minY, (int) x1);
    379                            this.maxIcon.paintIcon(null, g2, (int) maxY, (int) x1);
    380                        }
    381                    }
    382                }
    383                else {  // reset the min and max
    384                    this.lastCategory = column;
    385                    this.min = value.doubleValue();
    386                    this.max = value.doubleValue();
    387                }
    388
    389                // connect to the previous point
    390                if (this.plotLines) {
    391                    if (column != 0) {
    392                        Number previousValue = dataset.getValue(row, column - 1);
    393                        if (previousValue != null) {
    394                            // previous data point...
    395                            double previous = previousValue.doubleValue();
    396                            double x0 = domainAxis.getCategoryMiddle(column - 1,
    397                                    getColumnCount(), dataArea,
    398                                    plot.getDomainAxisEdge());
    399                            double y0 = rangeAxis.valueToJava2D(previous, dataArea,
    400                                    plot.getRangeAxisEdge());
    401                            g2.setPaint(getItemPaint(row, column));
    402                            g2.setStroke(getItemStroke(row, column));
    403                            Line2D line;
    404                            if (orient == PlotOrientation.VERTICAL) {
    405                                line = new Line2D.Double(x0, y0, x1, y1);
    406                            }
    407                            else {
    408                                line = new Line2D.Double(y0, x0, y1, x1);
    409                            }
    410                            g2.draw(line);
    411                        }
    412                    }
    413                }
    414
    415                // add an item entity, if this information is being collected
    416                EntityCollection entities = state.getEntityCollection();
    417                if (entities != null && shape != null) {
    418                    addItemEntity(entities, dataset, row, column, shape);
    419                }
    420            }
    421        }
    422
    423        /**
    424         * Tests this instance for equality with an arbitrary object.  The icon
    425         * fields are NOT included in the test, so this implementation is a little
    426         * weak.
    427         *
    428         * @param obj  the object (<code>null</code> permitted).
    429         *
    430         * @return A boolean.
    431         *
    432         * @since 1.0.7
    433         */
    434        public boolean equals(Object obj) {
    435            if (obj == this) {
    436                return true;
    437            }
    438            if (!(obj instanceof MinMaxCategoryRenderer)) {
    439                return false;
    440            }
    441            MinMaxCategoryRenderer that = (MinMaxCategoryRenderer) obj;
    442            if (this.plotLines != that.plotLines) {
    443                return false;
    444            }
    445            if (!PaintUtilities.equal(this.groupPaint, that.groupPaint)) {
    446                return false;
    447            }
    448            if (!this.groupStroke.equals(that.groupStroke)) {
    449                return false;
    450            }
    451            return super.equals(obj);
    452        }
    453
    454        /**
    455         * Returns an icon.
    456         *
    457         * @param shape  the shape.
    458         * @param fillPaint  the fill paint.
    459         * @param outlinePaint  the outline paint.
    460         *
    461         * @return The icon.
    462         */
    463        private Icon getIcon(Shape shape, final Paint fillPaint,
    464                            final Paint outlinePaint) {
    465
    466          final int width = shape.getBounds().width;
    467          final int height = shape.getBounds().height;
    468          final GeneralPath path = new GeneralPath(shape);
    469          return new Icon() {
    470              public void paintIcon(Component c, Graphics g, int x, int y) {
    471                  Graphics2D g2 = (Graphics2D) g;
    472                  path.transform(AffineTransform.getTranslateInstance(x, y));
    473                  if (fillPaint != null) {
    474                      g2.setPaint(fillPaint);
    475                      g2.fill(path);
    476                  }
    477                  if (outlinePaint != null) {
    478                      g2.setPaint(outlinePaint);
    479                      g2.draw(path);
    480                  }
    481                  path.transform(AffineTransform.getTranslateInstance(-x, -y));
    482            }
    483
    484            public int getIconWidth() {
    485                return width;
    486            }
    487
    488            public int getIconHeight() {
    489                return height;
    490            }
    491
    492          };
    493        }
    494
    495        /**
    496         * Returns an icon from a shape.
    497         *
    498         * @param shape  the shape.
    499         * @param fill  the fill flag.
    500         * @param outline  the outline flag.
    501         *
    502         * @return The icon.
    503         */
    504        private Icon getIcon(Shape shape, final boolean fill,
    505                final boolean outline) {
    506            final int width = shape.getBounds().width;
    507            final int height = shape.getBounds().height;
    508            final GeneralPath path = new GeneralPath(shape);
    509            return new Icon() {
    510                public void paintIcon(Component c, Graphics g, int x, int y) {
    511                    Graphics2D g2 = (Graphics2D) g;
    512                    path.transform(AffineTransform.getTranslateInstance(x, y));
    513                    if (fill) {
    514                        g2.fill(path);
    515                    }
    516                    if (outline) {
    517                        g2.draw(path);
    518                    }
    519                    path.transform(AffineTransform.getTranslateInstance(-x, -y));
    520                }
    521
    522                public int getIconWidth() {
    523                    return width;
    524                }
    525
    526                public int getIconHeight() {
    527                    return height;
    528                }
    529            };
    530        }
    531
    532        /**
    533         * Provides serialization support.
    534         *
    535         * @param stream  the output stream.
    536         *
    537         * @throws IOException  if there is an I/O error.
    538         */
    539        private void writeObject(ObjectOutputStream stream) throws IOException {
    540            stream.defaultWriteObject();
    541            SerialUtilities.writeStroke(this.groupStroke, stream);
    542            SerialUtilities.writePaint(this.groupPaint, stream);
    543        }
    544
    545        /**
    546         * Provides serialization support.
    547         *
    548         * @param stream  the input stream.
    549         *
    550         * @throws IOException  if there is an I/O error.
    551         * @throws ClassNotFoundException  if there is a classpath problem.
    552         */
    553        private void readObject(ObjectInputStream stream)
    554            throws IOException, ClassNotFoundException {
    555            stream.defaultReadObject();
    556            this.groupStroke = SerialUtilities.readStroke(stream);
    557            this.groupPaint = SerialUtilities.readPaint(stream);
    558
    559            this.minIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0, 360,
    560                    Arc2D.OPEN), null, Color.black);
    561            this.maxIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0, 360,
    562                    Arc2D.OPEN), null, Color.black);
    563            this.objectIcon = getIcon(new Line2D.Double(-4, 0, 4, 0), false, true);
    564        }
    565
    566    }