001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2007, 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-2007, 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    }