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     * CandlestickRenderer.java
029     * ------------------------
030     * (C) Copyright 2001-2004, by Object Refinery Limited.
031     *
032     * Original Authors:  David Gilbert (for Object Refinery Limited);
033     *                    Sylvain Vieujot;
034     * Contributor(s):    Richard Atkinson;
035     *                    Christian W. Zuckschwerdt;
036     *                    Jerome Fisher;
037     *
038     * $Id: CandlestickRenderer.java,v 1.7.2.1 2005/10/25 20:56:21 mungady Exp $
039     *
040     * Changes
041     * -------
042     * 13-Dec-2001 : Version 1.  Based on code in the (now redundant) 
043     *               CandlestickPlot class, written by Sylvain Vieujot (DG);
044     * 23-Jan-2002 : Added DrawInfo parameter to drawItem() method (DG);
045     * 28-Mar-2002 : Added a property change listener mechanism so that renderers 
046     *               no longer need to be immutable.  Added properties for up and 
047     *               down colors (DG);
048     * 04-Apr-2002 : Updated with new automatic width calculation and optional 
049     *               volume display, contributed by Sylvain Vieujot (DG);
050     * 09-Apr-2002 : Removed translatedRangeZero from the drawItem() method, and 
051     *               changed the return type of the drawItem method to void, 
052     *               reflecting a change in the XYItemRenderer interface.  Added 
053     *               tooltip code to drawItem() method (DG);
054     * 25-Jun-2002 : Removed redundant code (DG);
055     * 05-Aug-2002 : Small modification to drawItem method to support URLs for HTML 
056     *               image maps (RA);
057     * 19-Sep-2002 : Fixed errors reported by Checkstyle (DG);
058     * 25-Mar-2003 : Implemented Serializable (DG);
059     * 01-May-2003 : Modified drawItem() method signature (DG);
060     * 30-Jun-2003 : Added support for PlotOrientation (for completeness, this 
061     *               renderer is unlikely to be used with a HORIZONTAL 
062     *               orientation) (DG);
063     * 30-Jul-2003 : Modified entity constructor (CZ);
064     * 20-Aug-2003 : Implemented Cloneable and PublicCloneable (DG);
065     * 29-Aug-2003 : Moved maxVolume calculation to initialise method (see bug 
066     *               report 796619) (DG);
067     * 02-Sep-2003 : Added maxCandleWidthInMilliseconds as workaround for bug 
068     *               796621 (DG);
069     * 08-Sep-2003 : Changed ValueAxis API (DG);
070     * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
071     * 13-Oct-2003 : Applied patch from Jerome Fisher to improve auto width 
072     *               calculations (DG);
073     * 23-Dec-2003 : Fixed bug where up and down paint are used incorrectly (DG);
074     * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState (DG);
075     * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 
076     *               getYValue() (DG);
077     * 
078     */
079    
080    package org.jfree.chart.renderer.xy;
081    
082    import java.awt.AlphaComposite;
083    import java.awt.Color;
084    import java.awt.Composite;
085    import java.awt.Graphics2D;
086    import java.awt.Paint;
087    import java.awt.Shape;
088    import java.awt.Stroke;
089    import java.awt.geom.Line2D;
090    import java.awt.geom.Rectangle2D;
091    import java.io.IOException;
092    import java.io.ObjectInputStream;
093    import java.io.ObjectOutputStream;
094    import java.io.Serializable;
095    
096    import org.jfree.chart.axis.ValueAxis;
097    import org.jfree.chart.entity.EntityCollection;
098    import org.jfree.chart.entity.XYItemEntity;
099    import org.jfree.chart.event.RendererChangeEvent;
100    import org.jfree.chart.labels.HighLowItemLabelGenerator;
101    import org.jfree.chart.labels.XYToolTipGenerator;
102    import org.jfree.chart.plot.CrosshairState;
103    import org.jfree.chart.plot.PlotOrientation;
104    import org.jfree.chart.plot.PlotRenderingInfo;
105    import org.jfree.chart.plot.XYPlot;
106    import org.jfree.data.xy.IntervalXYDataset;
107    import org.jfree.data.xy.OHLCDataset;
108    import org.jfree.data.xy.XYDataset;
109    import org.jfree.io.SerialUtilities;
110    import org.jfree.ui.RectangleEdge;
111    import org.jfree.util.PublicCloneable;
112    
113    /**
114     * A renderer that draws candlesticks on an {@link XYPlot} (requires a 
115     * {@link OHLCDataset}).
116     * <P>
117     * This renderer does not include code to calculate the crosshair point for the 
118     * plot.
119     *
120     * @author Sylvain Vieujot
121     */
122    public class CandlestickRenderer extends AbstractXYItemRenderer 
123                                     implements XYItemRenderer, 
124                                                Cloneable,
125                                                PublicCloneable,
126                                                Serializable {
127                
128        /** For serialization. */
129        private static final long serialVersionUID = 50390395841817121L;
130        
131        /** The average width method. */                                          
132        public static final int WIDTHMETHOD_AVERAGE = 0;
133        
134        /** The smallest width method. */
135        public static final int WIDTHMETHOD_SMALLEST = 1;
136        
137        /** The interval data method. */
138        public static final int WIDTHMETHOD_INTERVALDATA = 2;
139    
140        /** The method of automatically calculating the candle width. */
141        private int autoWidthMethod = WIDTHMETHOD_AVERAGE;
142    
143        /** 
144         * The number (generally between 0.0 and 1.0) by which the available space 
145         * automatically calculated for the candles will be multiplied to determine
146         * the actual width to use. 
147         */
148        private double autoWidthFactor = 4.5 / 7;
149    
150        /** The minimum gap between one candle and the next */
151        private double autoWidthGap = 0.0;
152    
153        /** The candle width. */
154        private double candleWidth;
155        
156        /** The maximum candlewidth in milliseconds. */
157        private double maxCandleWidthInMilliseconds = 1000.0 * 60.0 * 60.0 * 20.0;
158        
159        /** Temporary storage for the maximum candle width. */
160        private double maxCandleWidth;
161    
162        /** 
163         * The paint used to fill the candle when the price moved up from open to 
164         * close. 
165         */
166        private transient Paint upPaint;
167    
168        /** 
169         * The paint used to fill the candle when the price moved down from open 
170         * to close. 
171         */
172        private transient Paint downPaint;
173    
174        /** A flag controlling whether or not volume bars are drawn on the chart. */
175        private boolean drawVolume;
176        
177        /** Temporary storage for the maximum volume. */
178        private transient double maxVolume;
179    
180        /**
181         * Creates a new renderer for candlestick charts.
182         */
183        public CandlestickRenderer() {
184            this(-1.0);
185        }
186    
187        /**
188         * Creates a new renderer for candlestick charts.
189         * <P>
190         * Use -1 for the candle width if you prefer the width to be calculated 
191         * automatically.
192         *
193         * @param candleWidth  The candle width.
194         */
195        public CandlestickRenderer(double candleWidth) {
196            this(candleWidth, true, new HighLowItemLabelGenerator());
197        }
198    
199        /**
200         * Creates a new renderer for candlestick charts.
201         * <P>
202         * Use -1 for the candle width if you prefer the width to be calculated 
203         * automatically.
204         *
205         * @param candleWidth  the candle width.
206         * @param drawVolume  a flag indicating whether or not volume bars should 
207         *                    be drawn.
208         * @param toolTipGenerator  the tool tip generator. <code>null</code> is 
209         *                          none.
210         */
211        public CandlestickRenderer(double candleWidth, boolean drawVolume,
212                                   XYToolTipGenerator toolTipGenerator) {
213    
214            super();
215            setToolTipGenerator(toolTipGenerator);
216            this.candleWidth = candleWidth;
217            this.drawVolume = drawVolume;
218            this.upPaint = Color.green;
219            this.downPaint = Color.red;
220    
221        }
222    
223        /**
224         * Returns the width of each candle.
225         *
226         * @return The candle width.
227         * 
228         * @see #setCandleWidth(double)
229         */
230        public double getCandleWidth() {
231            return this.candleWidth;
232        }
233    
234        /**
235         * Sets the candle width.
236         * <P>
237         * If you set the width to a negative value, the renderer will calculate
238         * the candle width automatically based on the space available on the chart.
239         *
240         * @param width  The width.
241         * @see #setAutoWidthMethod(int)
242         * @see #setAutoWidthGap(double)
243         * @see #setAutoWidthFactor(double)
244         * @see #setMaxCandleWidthInMilliseconds(double)
245         */
246        public void setCandleWidth(double width) {
247            if (width != this.candleWidth) {
248                this.candleWidth = width;
249                notifyListeners(new RendererChangeEvent(this));
250            }
251        }
252    
253        /**
254         * Returns the maximum width (in milliseconds) of each candle.
255         *
256         * @return The maximum candle width in milliseconds.
257         */
258        public double getMaxCandleWidthInMilliseconds() {
259            return this.maxCandleWidthInMilliseconds;
260        }
261    
262        /**
263         * Sets the maximum candle width (in milliseconds).  
264         *
265         * @param millis  The maximum width.
266         * @see #setCandleWidth(double)
267         * @see #setAutoWidthMethod(int)
268         * @see #setAutoWidthGap(double)
269         * @see #setAutoWidthFactor(double)
270         */
271        public void setMaxCandleWidthInMilliseconds(double millis) {
272            this.maxCandleWidthInMilliseconds = millis;
273            notifyListeners(new RendererChangeEvent(this));
274        }
275    
276        /**
277         * Returns the method of automatically calculating the candle width.
278         *
279         * @return The method of automatically calculating the candle width.
280         */
281        public int getAutoWidthMethod() {
282            return this.autoWidthMethod;
283        }
284    
285        /**
286         * Sets the method of automatically calculating the candle width.
287         * <p>
288         * <code>WIDTHMETHOD_AVERAGE</code>: Divides the entire display (ignoring 
289         * scale factor) by the number of items, and uses this as the available 
290         * width.<br>
291         * <code>WIDTHMETHOD_SMALLEST</code>: Checks the interval between each 
292         * item, and uses the smallest as the available width.<br>
293         * <code>WIDTHMETHOD_INTERVALDATA</code>: Assumes that the dataset supports
294         * the IntervalXYDataset interface, and uses the startXValue - endXValue as 
295         * the available width.
296         * <br>
297         *
298         * @param autoWidthMethod  The method of automatically calculating the 
299         * candle width.
300         *
301         * @see #WIDTHMETHOD_AVERAGE
302         * @see #WIDTHMETHOD_SMALLEST
303         * @see #WIDTHMETHOD_INTERVALDATA
304         * @see #setCandleWidth(double)
305         * @see #setAutoWidthGap(double)
306         * @see #setAutoWidthFactor(double)
307         * @see #setMaxCandleWidthInMilliseconds(double)
308         */
309        public void setAutoWidthMethod(int autoWidthMethod) {
310            if (this.autoWidthMethod != autoWidthMethod) {
311                this.autoWidthMethod = autoWidthMethod;
312                notifyListeners(new RendererChangeEvent(this));
313            }
314        }
315    
316        /**
317         * Returns the factor by which the available space automatically 
318         * calculated for the candles will be multiplied to determine the actual 
319         * width to use.
320         *
321         * @return The width factor (generally between 0.0 and 1.0).
322         */
323        public double getAutoWidthFactor() {
324            return this.autoWidthFactor;
325        }
326    
327        /**
328         * Sets the factor by which the available space automatically calculated 
329         * for the candles will be multiplied to determine the actual width to use.
330         *
331         * @param autoWidthFactor The width factor (generally between 0.0 and 1.0).
332         * @see #setCandleWidth(double)
333         * @see #setAutoWidthMethod(int)
334         * @see #setAutoWidthGap(double)
335         * @see #setMaxCandleWidthInMilliseconds(double)
336         */
337        public void setAutoWidthFactor(double autoWidthFactor) {
338            if (this.autoWidthFactor != autoWidthFactor) {
339                this.autoWidthFactor = autoWidthFactor;
340                notifyListeners(new RendererChangeEvent(this));
341            }
342        }
343    
344        /**
345         * Returns the amount of space to leave on the left and right of each 
346         * candle when automatically calculating widths.
347         *
348         * @return The gap.
349         */
350        public double getAutoWidthGap() {
351            return this.autoWidthGap;
352        }
353    
354        /**
355         * Sets the amount of space to leave on the left and right of each candle 
356         * when automatically calculating widths.
357         *
358         * @param autoWidthGap The gap.
359         * @see #setCandleWidth(double)
360         * @see #setAutoWidthMethod(int)
361         * @see #setAutoWidthFactor(double)
362         * @see #setMaxCandleWidthInMilliseconds(double)
363         */
364        public void setAutoWidthGap(double autoWidthGap) {
365            if (this.autoWidthGap != autoWidthGap) {
366                this.autoWidthGap = autoWidthGap;
367                notifyListeners(new RendererChangeEvent(this));
368            }
369        }
370    
371        /**
372         * Returns the paint used to fill candles when the price moves up from open
373         * to close.
374         *
375         * @return The paint.
376         */
377        public Paint getUpPaint() {
378            return this.upPaint;
379        }
380    
381        /**
382         * Sets the paint used to fill candles when the price moves up from open
383         * to close.
384         * <P>
385         * Registered property change listeners are notified that the
386         * "CandleStickRenderer.upPaint" property has changed.
387         *
388         * @param paint The paint.
389         */
390        public void setUpPaint(Paint paint) {
391            this.upPaint = paint;
392            notifyListeners(new RendererChangeEvent(this));
393        }
394    
395        /**
396         * Returns the paint used to fill candles when the price moves down from
397         * open to close.
398         *
399         * @return The paint.
400         */
401        public Paint getDownPaint() {
402            return this.downPaint;
403        }
404    
405        /**
406         * Sets the paint used to fill candles when the price moves down from open
407         * to close.
408         * <P>
409         * Registered property change listeners are notified that the
410         * "CandleStickRenderer.downPaint" property has changed.
411         *
412         * @param paint  The paint.
413         */
414        public void setDownPaint(Paint paint) {
415            this.downPaint = paint;
416            notifyListeners(new RendererChangeEvent(this));
417        }
418    
419        /**
420         * Returns a flag indicating whether or not volume bars are drawn on the
421         * chart.
422         *
423         * @return <code>true</code> if volume bars are drawn on the chart.
424         */
425        public boolean drawVolume() {
426            return this.drawVolume;
427        }
428    
429        /**
430         * Sets a flag that controls whether or not volume bars are drawn in the
431         * background.
432         *
433         * @param flag The flag.
434         */
435        public void setDrawVolume(boolean flag) {
436            if (this.drawVolume != flag) {
437                this.drawVolume = flag;
438                notifyListeners(new RendererChangeEvent(this));
439            }
440        }
441    
442        /**
443         * Initialises the renderer then returns the number of 'passes' through the
444         * data that the renderer will require (usually just one).  This method 
445         * will be called before the first item is rendered, giving the renderer 
446         * an opportunity to initialise any state information it wants to maintain.
447         * The renderer can do nothing if it chooses.
448         *
449         * @param g2  the graphics device.
450         * @param dataArea  the area inside the axes.
451         * @param plot  the plot.
452         * @param dataset  the data.
453         * @param info  an optional info collection object to return data back to 
454         *              the caller.
455         *
456         * @return The number of passes the renderer requires.
457         */
458        public XYItemRendererState initialise(Graphics2D g2,
459                                              Rectangle2D dataArea,
460                                              XYPlot plot,
461                                              XYDataset dataset,
462                                              PlotRenderingInfo info) {
463              
464            // calculate the maximum allowed candle width from the axis...
465            ValueAxis axis = plot.getDomainAxis();
466            double x1 = axis.getLowerBound();
467            double x2 = x1 + this.maxCandleWidthInMilliseconds;
468            RectangleEdge edge = plot.getDomainAxisEdge();
469            double xx1 = axis.valueToJava2D(x1, dataArea, edge);
470            double xx2 = axis.valueToJava2D(x2, dataArea, edge);
471            this.maxCandleWidth = Math.abs(xx2 - xx1); 
472                // Absolute value, since the relative x 
473                // positions are reversed for horizontal orientation
474            
475            // calculate the highest volume in the dataset... 
476            if (this.drawVolume) {
477                OHLCDataset highLowDataset = (OHLCDataset) dataset;
478                this.maxVolume = 0.0;
479                for (int series = 0; series < highLowDataset.getSeriesCount(); 
480                     series++) {
481                    for (int item = 0; item < highLowDataset.getItemCount(series); 
482                         item++) {
483                        double volume = highLowDataset.getVolumeValue(series, item);
484                        if (volume > this.maxVolume) {
485                            this.maxVolume = volume;
486                        }
487                        
488                    }    
489                }
490            }
491            
492            return new XYItemRendererState(info);
493        }
494    
495        /**
496         * Draws the visual representation of a single data item.
497         *
498         * @param g2  the graphics device.
499         * @param state  the renderer state.
500         * @param dataArea  the area within which the plot is being drawn.
501         * @param info  collects info about the drawing.
502         * @param plot  the plot (can be used to obtain standard color 
503         *              information etc).
504         * @param domainAxis  the domain axis.
505         * @param rangeAxis  the range axis.
506         * @param dataset  the dataset.
507         * @param series  the series index (zero-based).
508         * @param item  the item index (zero-based).
509         * @param crosshairState  crosshair information for the plot 
510         *                        (<code>null</code> permitted).
511         * @param pass  the pass index.
512         */
513        public void drawItem(Graphics2D g2, 
514                             XYItemRendererState state,
515                             Rectangle2D dataArea,
516                             PlotRenderingInfo info,
517                             XYPlot plot, 
518                             ValueAxis domainAxis, 
519                             ValueAxis rangeAxis,
520                             XYDataset dataset, 
521                             int series, 
522                             int item,
523                             CrosshairState crosshairState,
524                             int pass) {
525    
526            boolean horiz;
527            PlotOrientation orientation = plot.getOrientation();
528            if (orientation == PlotOrientation.HORIZONTAL) {
529                horiz = true;
530            }
531            else if (orientation == PlotOrientation.VERTICAL) {
532                horiz = false;
533            }
534            else {
535                return;
536            }
537            
538            // setup for collecting optional entity info...
539            EntityCollection entities = null;
540            if (info != null) {
541                entities = info.getOwner().getEntityCollection();
542            }
543    
544            OHLCDataset highLowData = (OHLCDataset) dataset;
545    
546            Number x = highLowData.getX(series, item);
547            Number yHigh = highLowData.getHigh(series, item);
548            Number yLow = highLowData.getLow(series, item);
549            Number yOpen = highLowData.getOpen(series, item);
550            Number yClose = highLowData.getClose(series, item);
551    
552            RectangleEdge domainEdge = plot.getDomainAxisEdge();
553            double xx = domainAxis.valueToJava2D(x.doubleValue(), dataArea, 
554                    domainEdge);
555    
556            RectangleEdge edge = plot.getRangeAxisEdge();
557            double yyHigh = rangeAxis.valueToJava2D(yHigh.doubleValue(), dataArea,
558                    edge);
559            double yyLow = rangeAxis.valueToJava2D(yLow.doubleValue(), dataArea, 
560                    edge);
561            double yyOpen = rangeAxis.valueToJava2D(yOpen.doubleValue(), dataArea, 
562                    edge);
563            double yyClose = rangeAxis.valueToJava2D(yClose.doubleValue(), dataArea,
564                    edge);
565    
566            double volumeWidth;
567            double stickWidth;
568            if (this.candleWidth > 0) {
569                // These are deliberately not bounded to minimums/maxCandleWidth to
570                //  retain old behaviour.
571                volumeWidth = this.candleWidth;
572                stickWidth = this.candleWidth;
573            }
574            else {
575                double xxWidth = 0;
576                int itemCount;
577                switch (this.autoWidthMethod) {
578                
579                    case WIDTHMETHOD_AVERAGE:
580                        itemCount = highLowData.getItemCount(series);
581                        if (horiz) {
582                            xxWidth = dataArea.getHeight() / itemCount;
583                        }
584                        else {
585                            xxWidth = dataArea.getWidth() / itemCount;
586                        }
587                        break;
588                
589                    case WIDTHMETHOD_SMALLEST:
590                        // Note: It would be nice to pre-calculate this per series
591                        itemCount = highLowData.getItemCount(series);
592                        double lastPos = -1;
593                        xxWidth = dataArea.getWidth();
594                        for (int i = 0; i < itemCount; i++) {
595                            double pos = domainAxis.valueToJava2D(
596                                highLowData.getXValue(series, i), dataArea, 
597                                domainEdge
598                            );
599                            if (lastPos != -1) {
600                                xxWidth = Math.min(
601                                    xxWidth, Math.abs(pos - lastPos)
602                                );
603                            }
604                            lastPos = pos;
605                        }
606                        break;
607                
608                    case WIDTHMETHOD_INTERVALDATA:
609                        IntervalXYDataset intervalXYData 
610                            = (IntervalXYDataset) dataset;
611                        double startPos = domainAxis.valueToJava2D(
612                            intervalXYData.getStartXValue(series, item), dataArea, 
613                            plot.getDomainAxisEdge()
614                        );
615                        double endPos = domainAxis.valueToJava2D(
616                            intervalXYData.getEndXValue(series, item), dataArea, 
617                            plot.getDomainAxisEdge()
618                        );
619                        xxWidth = Math.abs(endPos - startPos);
620                        break;
621                    
622                }
623                xxWidth -= 2 * this.autoWidthGap;
624                xxWidth *= this.autoWidthFactor;
625                xxWidth = Math.min(xxWidth, this.maxCandleWidth);
626                volumeWidth = Math.max(Math.min(1, this.maxCandleWidth), xxWidth);
627                stickWidth = Math.max(Math.min(3, this.maxCandleWidth), xxWidth);
628            }
629    
630            Paint p = getItemPaint(series, item);
631            Stroke s = getItemStroke(series, item);
632    
633            g2.setStroke(s);
634    
635            if (this.drawVolume) {
636                int volume = (int) highLowData.getVolumeValue(series, item);
637                double volumeHeight = volume / this.maxVolume;
638    
639                double min, max;
640                if (horiz) {
641                    min = dataArea.getMinX();
642                    max = dataArea.getMaxX();
643                }
644                else {
645                    min = dataArea.getMinY();
646                    max = dataArea.getMaxY();
647                }
648    
649                double zzVolume = volumeHeight * (max - min);
650    
651                g2.setPaint(Color.gray);
652                Composite originalComposite = g2.getComposite();
653                g2.setComposite(
654                    AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f)
655                );
656    
657                if (horiz) {
658                    g2.fill(new Rectangle2D.Double(min,
659                                                   xx - volumeWidth / 2,
660                                                   zzVolume, volumeWidth));
661                }
662                else {
663                    g2.fill(
664                        new Rectangle2D.Double(
665                            xx - volumeWidth / 2,
666                            max - zzVolume, volumeWidth, zzVolume
667                        )
668                    );
669                }
670    
671                g2.setComposite(originalComposite);
672            }
673    
674            g2.setPaint(p);
675    
676            double yyMaxOpenClose = Math.max(yyOpen, yyClose);
677            double yyMinOpenClose = Math.min(yyOpen, yyClose);
678            double maxOpenClose = Math.max(yOpen.doubleValue(), 
679                    yClose.doubleValue());
680            double minOpenClose = Math.min(yOpen.doubleValue(), 
681                    yClose.doubleValue());
682    
683            // draw the upper shadow
684            if (yHigh.doubleValue() > maxOpenClose) {
685                if (horiz) {
686                    g2.draw(new Line2D.Double(yyHigh, xx, yyMaxOpenClose, xx));
687                }
688                else {
689                    g2.draw(new Line2D.Double(xx, yyHigh, xx, yyMaxOpenClose));
690                }
691            }
692    
693            // draw the lower shadow
694            if (yLow.doubleValue() < minOpenClose) {
695                if (horiz) {
696                    g2.draw(new Line2D.Double(yyLow, xx, yyMinOpenClose, xx));
697                }
698                else {
699                    g2.draw(new Line2D.Double(xx, yyLow, xx, yyMinOpenClose));
700                }
701            }
702    
703            // draw the body
704            Shape body = null;
705            if (horiz) {
706                body = new Rectangle2D.Double(
707                    yyMinOpenClose, xx - stickWidth / 2, 
708                    yyMaxOpenClose - yyMinOpenClose, stickWidth
709                );
710            } 
711            else {
712                body = new Rectangle2D.Double(
713                    xx - stickWidth / 2, yyMinOpenClose,
714                    stickWidth, yyMaxOpenClose - yyMinOpenClose
715                );
716            }
717            if (yClose.doubleValue() > yOpen.doubleValue()) {
718                if (this.upPaint != null) {
719                    g2.setPaint(this.upPaint);
720                    g2.fill(body);
721                }
722            }
723            else {
724                if (this.downPaint != null) {
725                    g2.setPaint(this.downPaint);
726                }
727                g2.fill(body);
728            }
729            g2.setPaint(p);
730            g2.draw(body);
731    
732            // add an entity for the item...
733            if (entities != null) {
734                String tip = null;
735                XYToolTipGenerator generator = getToolTipGenerator(series, item);
736                if (generator != null) {
737                    tip = generator.generateToolTip(dataset, series, item);
738                }
739                String url = null;
740                if (getURLGenerator() != null) {
741                    url = getURLGenerator().generateURL(dataset, series, item);
742                }
743                XYItemEntity entity = new XYItemEntity(body, dataset, series, item, 
744                        tip, url);
745                entities.add(entity);
746            }
747    
748        }
749    
750        /**
751         * Tests this renderer for equality with another object.
752         *
753         * @param obj  the object.
754         *
755         * @return <code>true</code> or <code>false</code>.
756         */
757        public boolean equals(Object obj) {
758    
759            if (obj == null) {
760                return false;
761            }
762    
763            if (obj == this) {
764                return true;
765            }
766    
767            if (obj instanceof CandlestickRenderer) {
768                CandlestickRenderer renderer = (CandlestickRenderer) obj;
769                boolean result = super.equals(obj);
770                result = result && (this.candleWidth == renderer.getCandleWidth());
771                result = result && (this.upPaint.equals(renderer.getUpPaint()));
772                result = result && (this.downPaint.equals(renderer.getDownPaint()));
773                result = result && (this.drawVolume == renderer.drawVolume);
774                return result;
775            }
776    
777            return false;
778    
779        }
780    
781        /**
782         * Returns a clone of the renderer.
783         * 
784         * @return A clone.
785         * 
786         * @throws CloneNotSupportedException  if the renderer cannot be cloned.
787         */
788        public Object clone() throws CloneNotSupportedException {
789            return super.clone();
790        }
791    
792        /**
793         * Provides serialization support.
794         *
795         * @param stream  the output stream.
796         *
797         * @throws IOException  if there is an I/O error.
798         */
799        private void writeObject(ObjectOutputStream stream) throws IOException {
800            stream.defaultWriteObject();
801            SerialUtilities.writePaint(this.upPaint, stream);
802            SerialUtilities.writePaint(this.downPaint, stream);
803        }
804    
805        /**
806         * Provides serialization support.
807         *
808         * @param stream  the input stream.
809         *
810         * @throws IOException  if there is an I/O error.
811         * @throws ClassNotFoundException  if there is a classpath problem.
812         */
813        private void readObject(ObjectInputStream stream) 
814                throws IOException, ClassNotFoundException {
815            stream.defaultReadObject();
816            this.upPaint = SerialUtilities.readPaint(stream);
817            this.downPaint = SerialUtilities.readPaint(stream);
818        }
819        
820    }