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