001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2005, 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-2005, 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.5 2005/12/02 10:05:57 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     * 
056     */
057    
058    package org.jfree.chart.renderer.category;
059    
060    import java.awt.BasicStroke;
061    import java.awt.Color;
062    import java.awt.Component;
063    import java.awt.Graphics;
064    import java.awt.Graphics2D;
065    import java.awt.Paint;
066    import java.awt.Shape;
067    import java.awt.Stroke;
068    import java.awt.geom.AffineTransform;
069    import java.awt.geom.Arc2D;
070    import java.awt.geom.GeneralPath;
071    import java.awt.geom.Line2D;
072    import java.awt.geom.Rectangle2D;
073    import java.io.IOException;
074    import java.io.ObjectInputStream;
075    import java.io.ObjectOutputStream;
076    
077    import javax.swing.Icon;
078    
079    import org.jfree.chart.axis.CategoryAxis;
080    import org.jfree.chart.axis.ValueAxis;
081    import org.jfree.chart.entity.CategoryItemEntity;
082    import org.jfree.chart.entity.EntityCollection;
083    import org.jfree.chart.event.RendererChangeEvent;
084    import org.jfree.chart.labels.CategoryToolTipGenerator;
085    import org.jfree.chart.plot.CategoryPlot;
086    import org.jfree.data.category.CategoryDataset;
087    import org.jfree.io.SerialUtilities;
088    
089    /**
090     * Renderer for drawing min max plot. This renderer draws all the series under 
091     * the same category in the same x position using <code>objectIcon</code> and 
092     * a line from the maximum value to the minimum value.
093     * <p>
094     * For use with the {@link org.jfree.chart.plot.CategoryPlot} class.
095     *
096     * @author Tomer Peretz
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                this.notifyListeners(new RendererChangeEvent(this));
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            notifyListeners(new RendererChangeEvent(this));
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.
217         *
218         * @param groupStroke The new stroke
219         */
220        public void setGroupStroke(Stroke groupStroke) {
221            this.groupStroke = groupStroke;
222        }
223    
224        /**
225         * Returns the icon drawn for each data item.
226         *
227         * @return The icon (never <code>null</code>).
228         * 
229         * @see #setObjectIcon(Icon)
230         */
231        public Icon getObjectIcon() {
232            return this.objectIcon;
233        }
234    
235        /**
236         * Sets the icon drawn for each data item.
237         *
238         * @param icon  the icon.
239         * 
240         * @see #getObjectIcon()
241         */
242        public void setObjectIcon(Icon icon) {
243            if (icon == null) {
244                throw new IllegalArgumentException("Null 'icon' argument.");
245            }
246            this.objectIcon = icon;
247            notifyListeners(new RendererChangeEvent(this));
248        }
249    
250        /**
251         * Returns the icon displayed for the maximum value data item within each
252         * category.
253         *
254         * @return The icon (never <code>null</code>).
255         * 
256         * @see #setMaxIcon(Icon)
257         */
258        public Icon getMaxIcon() {
259            return this.maxIcon;
260        }
261    
262        /**
263         * Sets the icon displayed for the maximum value data item within each
264         * category and sends a {@link RendererChangeEvent} to all registered
265         * listeners.
266         *
267         * @param icon  the icon (<code>null</code> not permitted).
268         * 
269         * @see #getMaxIcon()
270         */
271        public void setMaxIcon(Icon icon) {
272            if (icon == null) {
273                throw new IllegalArgumentException("Null 'icon' argument.");
274            }
275            this.maxIcon = icon;
276            notifyListeners(new RendererChangeEvent(this));
277        }
278    
279        /**
280         * Returns the icon displayed for the minimum value data item within each
281         * category.
282         *
283         * @return The icon (never <code>null</code>).
284         * 
285         * @see #setMinIcon(Icon)
286         */
287        public Icon getMinIcon() {
288            return this.minIcon;
289        }
290    
291        /**
292         * Sets the icon displayed for the minimum value data item within each
293         * category and sends a {@link RendererChangeEvent} to all registered
294         * listeners.
295         *
296         * @param icon  the icon (<code>null</code> not permitted).
297         * 
298         * @see #getMinIcon()
299         */
300        public void setMinIcon(Icon icon) {
301            if (icon == null) {
302                throw new IllegalArgumentException("Null 'icon' argument.");
303            }
304            this.minIcon = icon;
305            notifyListeners(new RendererChangeEvent(this));
306        }
307    
308        /**
309         * Draw a single data item.
310         *
311         * @param g2  the graphics device.
312         * @param state  the renderer state.
313         * @param dataArea  the area in which the data is drawn.
314         * @param plot  the plot.
315         * @param domainAxis  the domain axis.
316         * @param rangeAxis  the range axis.
317         * @param dataset  the dataset.
318         * @param row  the row index (zero-based).
319         * @param column  the column index (zero-based).
320         * @param pass  the pass index.
321         */
322        public void drawItem(Graphics2D g2, CategoryItemRendererState state,
323                Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis,
324                ValueAxis rangeAxis, CategoryDataset dataset, int row, int column,
325                int pass) {
326    
327            // first check the number we are plotting...
328            Number value = dataset.getValue(row, column);
329            if (value != null) {
330                // current data point...
331                double x1 = domainAxis.getCategoryMiddle(
332                    column, getColumnCount(), dataArea, plot.getDomainAxisEdge());
333                double y1 = rangeAxis.valueToJava2D(
334                    value.doubleValue(), dataArea, plot.getRangeAxisEdge());
335                g2.setPaint(getItemPaint(row, column));
336                g2.setStroke(getItemStroke(row, column));
337                Shape shape = null;
338                shape = new Rectangle2D.Double(x1 - 4, y1 - 4, 8.0, 8.0);
339                this.objectIcon.paintIcon(null, g2, (int) x1, (int) y1);
340                if (this.lastCategory == column) {
341                    if (this.min > value.doubleValue()) {
342                        this.min = value.doubleValue();
343                    }
344                    if (this.max < value.doubleValue()) {
345                        this.max = value.doubleValue();
346                    }
347                    if (dataset.getRowCount() - 1 == row) {
348                        g2.setPaint(this.groupPaint);
349                        g2.setStroke(this.groupStroke);
350                        double minY = rangeAxis.valueToJava2D(this.min, dataArea, 
351                                plot.getRangeAxisEdge());
352                        double maxY = rangeAxis.valueToJava2D(this.max, dataArea, 
353                                plot.getRangeAxisEdge());
354                        g2.draw(new Line2D.Double(x1, minY, x1, maxY));
355                        this.minIcon.paintIcon(null, g2, (int) x1, (int) minY);
356                        this.maxIcon.paintIcon(null, g2, (int) x1, (int) maxY);
357                    }
358                }
359                else {  // reset the min and max
360                    this.lastCategory = column;
361                    this.min = value.doubleValue();
362                    this.max = value.doubleValue();
363                }
364                // connect to the previous point
365                if (this.plotLines) {
366                    if (column != 0) {
367                        Number previousValue = dataset.getValue(row, column - 1);
368                        if (previousValue != null) {
369                            // previous data point...
370                            double previous = previousValue.doubleValue();
371                            double x0 = domainAxis.getCategoryMiddle(
372                                column - 1, getColumnCount(), dataArea,
373                                plot.getDomainAxisEdge());
374                            double y0 = rangeAxis.valueToJava2D(
375                                previous, dataArea, plot.getRangeAxisEdge());
376                            g2.setPaint(getItemPaint(row, column));
377                            g2.setStroke(getItemStroke(row, column));
378                            Line2D line = new Line2D.Double(x0, y0, x1, y1);
379                            g2.draw(line);
380                        }
381                    }
382                }
383    
384                // collect entity and tool tip information...
385                if (state.getInfo() != null) {
386                    EntityCollection entities = state.getEntityCollection();
387                    if (entities != null && shape != null) {
388                        String tip = null;
389                        CategoryToolTipGenerator tipster 
390                            = getToolTipGenerator(row, column);
391                        if (tipster != null) {
392                            tip = tipster.generateToolTip(dataset, row, column);
393                        }
394                        CategoryItemEntity entity = new CategoryItemEntity(
395                            shape, tip, null, dataset, row, 
396                            dataset.getColumnKey(column), column);
397                        entities.add(entity);
398                    }
399                }
400            }
401        }
402    
403        /**
404         * Returns an icon.
405         *
406         * @param shape  the shape.
407         * @param fillPaint  the fill paint.
408         * @param outlinePaint  the outline paint.
409         *
410         * @return The icon.
411         */
412        private Icon getIcon(Shape shape, final Paint fillPaint, 
413                            final Paint outlinePaint) {
414    
415          final int width = shape.getBounds().width;
416          final int height = shape.getBounds().height;
417          final GeneralPath path = new GeneralPath(shape);
418          return new Icon() {
419              public void paintIcon(Component c, Graphics g, int x, int y) {
420                  Graphics2D g2 = (Graphics2D) g;
421                  path.transform(AffineTransform.getTranslateInstance(x, y));
422                  if (fillPaint != null) {
423                      g2.setPaint(fillPaint);
424                      g2.fill(path);
425                  }
426                  if (outlinePaint != null) {
427                      g2.setPaint(outlinePaint);
428                      g2.draw(path);
429                  }
430                  path.transform(AffineTransform.getTranslateInstance(-x, -y));
431            }
432    
433            public int getIconWidth() {
434                return width;
435            }
436    
437            public int getIconHeight() {
438                return height;
439            }
440    
441          };
442        }
443    
444        /**
445         * Returns an icon.
446         *
447         * @param shape  the shape.
448         * @param fill  the fill flag.
449         * @param outline  the outline flag.
450         *
451         * @return The icon.
452         */
453        private Icon getIcon(Shape shape, final boolean fill, 
454                             final boolean outline) {
455            final int width = shape.getBounds().width;
456            final int height = shape.getBounds().height;
457            final GeneralPath path = new GeneralPath(shape);
458            return new Icon() {
459                public void paintIcon(Component c, Graphics g, int x, int y) {
460                    Graphics2D g2 = (Graphics2D) g;
461                    path.transform(AffineTransform.getTranslateInstance(x, y));
462                    if (fill) {
463                        g2.fill(path);
464                    }
465                    if (outline) {
466                        g2.draw(path);
467                    }
468                    path.transform(AffineTransform.getTranslateInstance(-x, -y));
469                }
470    
471                public int getIconWidth() {
472                    return width;
473                }
474    
475                public int getIconHeight() {
476                    return height;
477                }
478            };
479        }
480        
481        /**
482         * Provides serialization support.
483         *
484         * @param stream  the output stream.
485         *
486         * @throws IOException  if there is an I/O error.
487         */
488        private void writeObject(ObjectOutputStream stream) throws IOException {
489            stream.defaultWriteObject();
490            SerialUtilities.writeStroke(this.groupStroke, stream);
491            SerialUtilities.writePaint(this.groupPaint, stream);
492        }
493        
494        /**
495         * Provides serialization support.
496         *
497         * @param stream  the input stream.
498         *
499         * @throws IOException  if there is an I/O error.
500         * @throws ClassNotFoundException  if there is a classpath problem.
501         */
502        private void readObject(ObjectInputStream stream) 
503            throws IOException, ClassNotFoundException {
504            stream.defaultReadObject();
505            this.groupStroke = SerialUtilities.readStroke(stream);
506            this.groupPaint = SerialUtilities.readPaint(stream);
507              
508            this.minIcon = getIcon(
509                new Arc2D.Double(-4, -4, 8, 8, 0, 360, Arc2D.OPEN), null, 
510                Color.black);
511            this.maxIcon = getIcon(
512                new Arc2D.Double(-4, -4, 8, 8, 0, 360, Arc2D.OPEN), null, 
513                Color.black);
514            this.objectIcon = getIcon(new Line2D.Double(-4, 0, 4, 0), false, true);
515        }
516        
517    }