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