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