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. 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    }