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 * CandlestickRenderer.java
029 * ------------------------
030 * (C) Copyright 2001-2007, 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 * Changes
039 * -------
040 * 13-Dec-2001 : Version 1.  Based on code in the (now redundant) 
041 *               CandlestickPlot class, written by Sylvain Vieujot (DG);
042 * 23-Jan-2002 : Added DrawInfo parameter to drawItem() method (DG);
043 * 28-Mar-2002 : Added a property change listener mechanism so that renderers 
044 *               no longer need to be immutable.  Added properties for up and 
045 *               down colors (DG);
046 * 04-Apr-2002 : Updated with new automatic width calculation and optional 
047 *               volume display, contributed by Sylvain Vieujot (DG);
048 * 09-Apr-2002 : Removed translatedRangeZero from the drawItem() method, and 
049 *               changed the return type of the drawItem method to void, 
050 *               reflecting a change in the XYItemRenderer interface.  Added 
051 *               tooltip code to drawItem() method (DG);
052 * 25-Jun-2002 : Removed redundant code (DG);
053 * 05-Aug-2002 : Small modification to drawItem method to support URLs for HTML 
054 *               image maps (RA);
055 * 19-Sep-2002 : Fixed errors reported by Checkstyle (DG);
056 * 25-Mar-2003 : Implemented Serializable (DG);
057 * 01-May-2003 : Modified drawItem() method signature (DG);
058 * 30-Jun-2003 : Added support for PlotOrientation (for completeness, this 
059 *               renderer is unlikely to be used with a HORIZONTAL 
060 *               orientation) (DG);
061 * 30-Jul-2003 : Modified entity constructor (CZ);
062 * 20-Aug-2003 : Implemented Cloneable and PublicCloneable (DG);
063 * 29-Aug-2003 : Moved maxVolume calculation to initialise method (see bug 
064 *               report 796619) (DG);
065 * 02-Sep-2003 : Added maxCandleWidthInMilliseconds as workaround for bug 
066 *               796621 (DG);
067 * 08-Sep-2003 : Changed ValueAxis API (DG);
068 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
069 * 13-Oct-2003 : Applied patch from Jerome Fisher to improve auto width 
070 *               calculations (DG);
071 * 23-Dec-2003 : Fixed bug where up and down paint are used incorrectly (DG);
072 * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState (DG);
073 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 
074 *               getYValue() (DG);
075 * ------------- JFREECHART 1.0.x ---------------------------------------------
076 * 06-Jul-2006 : Swapped calls to getX() --> getXValue(), and the same for the
077 *               other data values (DG);
078 * 17-Aug-2006 : Corrections to the equals() method (DG);
079 * 05-Mar-2007 : Added flag to allow optional use of outline paint (DG);
080 * 08-Oct-2007 : Added new volumePaint field (DG);
081 * 
082 */
083
084package org.jfree.chart.renderer.xy;
085
086import java.awt.AlphaComposite;
087import java.awt.Color;
088import java.awt.Composite;
089import java.awt.Graphics2D;
090import java.awt.Paint;
091import java.awt.Shape;
092import java.awt.Stroke;
093import java.awt.geom.Line2D;
094import java.awt.geom.Rectangle2D;
095import java.io.IOException;
096import java.io.ObjectInputStream;
097import java.io.ObjectOutputStream;
098import java.io.Serializable;
099
100import org.jfree.chart.axis.ValueAxis;
101import org.jfree.chart.entity.EntityCollection;
102import org.jfree.chart.entity.XYItemEntity;
103import org.jfree.chart.event.RendererChangeEvent;
104import org.jfree.chart.labels.HighLowItemLabelGenerator;
105import org.jfree.chart.labels.XYToolTipGenerator;
106import org.jfree.chart.plot.CrosshairState;
107import org.jfree.chart.plot.PlotOrientation;
108import org.jfree.chart.plot.PlotRenderingInfo;
109import org.jfree.chart.plot.XYPlot;
110import org.jfree.data.xy.IntervalXYDataset;
111import org.jfree.data.xy.OHLCDataset;
112import org.jfree.data.xy.XYDataset;
113import org.jfree.io.SerialUtilities;
114import org.jfree.ui.RectangleEdge;
115import org.jfree.util.PaintUtilities;
116import 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 */
125public 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    /** 
181     * The paint used to fill the volume bars (if they are visible).  Once 
182     * initialised, this field should never be set to <code>null</code>.
183     *
184     * @since 1.0.7
185     */
186    private transient Paint volumePaint;
187    
188    /** Temporary storage for the maximum volume. */
189    private transient double maxVolume;
190    
191    /** 
192     * A flag that controls whether or not the renderer's outline paint is
193     * used to draw the outline of the candlestick.  The default value is
194     * <code>false</code> to avoid a change of behaviour for existing code.
195     * 
196     * @since 1.0.5
197     */
198    private boolean useOutlinePaint;
199
200    /**
201     * Creates a new renderer for candlestick charts.
202     */
203    public CandlestickRenderer() {
204        this(-1.0);
205    }
206
207    /**
208     * Creates a new renderer for candlestick charts.
209     * <P>
210     * Use -1 for the candle width if you prefer the width to be calculated 
211     * automatically.
212     *
213     * @param candleWidth  The candle width.
214     */
215    public CandlestickRenderer(double candleWidth) {
216        this(candleWidth, true, new HighLowItemLabelGenerator());
217    }
218
219    /**
220     * Creates a new renderer for candlestick charts.
221     * <P>
222     * Use -1 for the candle width if you prefer the width to be calculated 
223     * automatically.
224     *
225     * @param candleWidth  the candle width.
226     * @param drawVolume  a flag indicating whether or not volume bars should 
227     *                    be drawn.
228     * @param toolTipGenerator  the tool tip generator. <code>null</code> is 
229     *                          none.
230     */
231    public CandlestickRenderer(double candleWidth, boolean drawVolume,
232                               XYToolTipGenerator toolTipGenerator) {
233        super();
234        setBaseToolTipGenerator(toolTipGenerator);
235        this.candleWidth = candleWidth;
236        this.drawVolume = drawVolume;
237        this.volumePaint = Color.gray;
238        this.upPaint = Color.green;
239        this.downPaint = Color.red;
240        this.useOutlinePaint = false;  // false preserves the old behaviour
241                                       // prior to introducing this flag
242    }
243
244    /**
245     * Returns the width of each candle.
246     *
247     * @return The candle width.
248     * 
249     * @see #setCandleWidth(double)
250     */
251    public double getCandleWidth() {
252        return this.candleWidth;
253    }
254
255    /**
256     * Sets the candle width.
257     * <P>
258     * If you set the width to a negative value, the renderer will calculate
259     * the candle width automatically based on the space available on the chart.
260     *
261     * @param width  The width.
262     * @see #setAutoWidthMethod(int)
263     * @see #setAutoWidthGap(double)
264     * @see #setAutoWidthFactor(double)
265     * @see #setMaxCandleWidthInMilliseconds(double)
266     */
267    public void setCandleWidth(double width) {
268        if (width != this.candleWidth) {
269            this.candleWidth = width;
270            notifyListeners(new RendererChangeEvent(this));
271        }
272    }
273
274    /**
275     * Returns the maximum width (in milliseconds) of each candle.
276     *
277     * @return The maximum candle width in milliseconds.
278     * 
279     * @see #setMaxCandleWidthInMilliseconds(double)
280     */
281    public double getMaxCandleWidthInMilliseconds() {
282        return this.maxCandleWidthInMilliseconds;
283    }
284
285    /**
286     * Sets the maximum candle width (in milliseconds).  
287     *
288     * @param millis  The maximum width.
289     * 
290     * @see #getMaxCandleWidthInMilliseconds()
291     * @see #setCandleWidth(double)
292     * @see #setAutoWidthMethod(int)
293     * @see #setAutoWidthGap(double)
294     * @see #setAutoWidthFactor(double)
295     */
296    public void setMaxCandleWidthInMilliseconds(double millis) {
297        this.maxCandleWidthInMilliseconds = millis;
298        notifyListeners(new RendererChangeEvent(this));
299    }
300
301    /**
302     * Returns the method of automatically calculating the candle width.
303     *
304     * @return The method of automatically calculating the candle width.
305     * 
306     * @see #setAutoWidthMethod(int)
307     */
308    public int getAutoWidthMethod() {
309        return this.autoWidthMethod;
310    }
311
312    /**
313     * Sets the method of automatically calculating the candle width.
314     * <p>
315     * <code>WIDTHMETHOD_AVERAGE</code>: Divides the entire display (ignoring 
316     * scale factor) by the number of items, and uses this as the available 
317     * width.<br>
318     * <code>WIDTHMETHOD_SMALLEST</code>: Checks the interval between each 
319     * item, and uses the smallest as the available width.<br>
320     * <code>WIDTHMETHOD_INTERVALDATA</code>: Assumes that the dataset supports
321     * the IntervalXYDataset interface, and uses the startXValue - endXValue as 
322     * the available width.
323     * <br>
324     *
325     * @param autoWidthMethod  The method of automatically calculating the 
326     * candle width.
327     *
328     * @see #WIDTHMETHOD_AVERAGE
329     * @see #WIDTHMETHOD_SMALLEST
330     * @see #WIDTHMETHOD_INTERVALDATA
331     * @see #getAutoWidthMethod()
332     * @see #setCandleWidth(double)
333     * @see #setAutoWidthGap(double)
334     * @see #setAutoWidthFactor(double)
335     * @see #setMaxCandleWidthInMilliseconds(double)
336     */
337    public void setAutoWidthMethod(int autoWidthMethod) {
338        if (this.autoWidthMethod != autoWidthMethod) {
339            this.autoWidthMethod = autoWidthMethod;
340            notifyListeners(new RendererChangeEvent(this));
341        }
342    }
343
344    /**
345     * Returns the factor by which the available space automatically 
346     * calculated for the candles will be multiplied to determine the actual 
347     * width to use.
348     *
349     * @return The width factor (generally between 0.0 and 1.0).
350     * 
351     * @see #setAutoWidthFactor(double)
352     */
353    public double getAutoWidthFactor() {
354        return this.autoWidthFactor;
355    }
356
357    /**
358     * Sets the factor by which the available space automatically calculated 
359     * for the candles will be multiplied to determine the actual width to use.
360     *
361     * @param autoWidthFactor The width factor (generally between 0.0 and 1.0).
362     * 
363     * @see #getAutoWidthFactor()
364     * @see #setCandleWidth(double)
365     * @see #setAutoWidthMethod(int)
366     * @see #setAutoWidthGap(double)
367     * @see #setMaxCandleWidthInMilliseconds(double)
368     */
369    public void setAutoWidthFactor(double autoWidthFactor) {
370        if (this.autoWidthFactor != autoWidthFactor) {
371            this.autoWidthFactor = autoWidthFactor;
372            notifyListeners(new RendererChangeEvent(this));
373        }
374    }
375
376    /**
377     * Returns the amount of space to leave on the left and right of each 
378     * candle when automatically calculating widths.
379     *
380     * @return The gap.
381     * 
382     * @see #setAutoWidthGap(double)
383     */
384    public double getAutoWidthGap() {
385        return this.autoWidthGap;
386    }
387
388    /**
389     * Sets the amount of space to leave on the left and right of each candle 
390     * when automatically calculating widths.
391     *
392     * @param autoWidthGap The gap.
393     * 
394     * @see #getAutoWidthGap()
395     * @see #setCandleWidth(double)
396     * @see #setAutoWidthMethod(int)
397     * @see #setAutoWidthFactor(double)
398     * @see #setMaxCandleWidthInMilliseconds(double)
399     */
400    public void setAutoWidthGap(double autoWidthGap) {
401        if (this.autoWidthGap != autoWidthGap) {
402            this.autoWidthGap = autoWidthGap;
403            notifyListeners(new RendererChangeEvent(this));
404        }
405    }
406
407    /**
408     * Returns the paint used to fill candles when the price moves up from open
409     * to close.
410     *
411     * @return The paint (possibly <code>null</code>).
412     * 
413     * @see #setUpPaint(Paint)
414     */
415    public Paint getUpPaint() {
416        return this.upPaint;
417    }
418
419    /**
420     * Sets the paint used to fill candles when the price moves up from open
421     * to close and sends a {@link RendererChangeEvent} to all registered
422     * listeners.
423     *
424     * @param paint  the paint (<code>null</code> permitted).
425     * 
426     * @see #getUpPaint()
427     */
428    public void setUpPaint(Paint paint) {
429        this.upPaint = paint;
430        notifyListeners(new RendererChangeEvent(this));
431    }
432
433    /**
434     * Returns the paint used to fill candles when the price moves down from
435     * open to close.
436     *
437     * @return The paint (possibly <code>null</code>).
438     * 
439     * @see #setDownPaint(Paint)
440     */
441    public Paint getDownPaint() {
442        return this.downPaint;
443    }
444
445    /**
446     * Sets the paint used to fill candles when the price moves down from open
447     * to close and sends a {@link RendererChangeEvent} to all registered
448     * listeners.
449     *
450     * @param paint  The paint (<code>null</code> permitted).
451     */
452    public void setDownPaint(Paint paint) {
453        this.downPaint = paint;
454        notifyListeners(new RendererChangeEvent(this));
455    }
456
457    /**
458     * Returns a flag indicating whether or not volume bars are drawn on the
459     * chart.
460     * 
461     * @return A boolean.
462     * 
463     * @since 1.0.5
464     * 
465     * @see #setDrawVolume(boolean)
466     */
467    public boolean getDrawVolume() {
468        return this.drawVolume;
469    }
470
471    /**
472     * Sets a flag that controls whether or not volume bars are drawn in the
473     * background and sends a {@link RendererChangeEvent} to all registered
474     * listeners.
475     *
476     * @param flag  the flag.
477     * 
478     * @see #getDrawVolume()
479     */
480    public void setDrawVolume(boolean flag) {
481        if (this.drawVolume != flag) {
482            this.drawVolume = flag;
483            notifyListeners(new RendererChangeEvent(this));
484        }
485    }
486    
487    /**
488     * Returns the paint that is used to fill the volume bars if they are
489     * visible.
490     * 
491     * @return The paint (never <code>null</code>).
492     * 
493     * @see #setVolumePaint(Paint)
494     * 
495     * @since 1.0.7
496     */
497    public Paint getVolumePaint() {
498        return this.volumePaint;    
499    }
500    
501    /**
502     * Sets the paint used to fill the volume bars, and sends a 
503     * {@link RendererChangeEvent} to all registered listeners.
504     * 
505     * @param paint  the paint (<code>null</code> not permitted).
506     * 
507     * @see #getVolumePaint()
508     * @see #getDrawVolume()
509     * 
510     * @since 1.0.7
511     */
512    public void setVolumePaint(Paint paint) {
513        if (paint == null) { 
514            throw new IllegalArgumentException("Null 'paint' argument.");
515        }
516        this.volumePaint = paint;
517        notifyListeners(new RendererChangeEvent(this));        
518    }
519
520    /**
521     * Returns the flag that controls whether or not the renderer's outline
522     * paint is used to draw the candlestick outline.  The default value is
523     * <code>false</code>.
524     * 
525     * @return A boolean.
526     * 
527     * @since 1.0.5
528     * 
529     * @see #setUseOutlinePaint(boolean)
530     */
531    public boolean getUseOutlinePaint() {
532        return this.useOutlinePaint;
533    }
534    
535    /**
536     * Sets the flag that controls whether or not the renderer's outline
537     * paint is used to draw the candlestick outline, and sends a 
538     * {@link RendererChangeEvent} to all registered listeners.
539     * 
540     * @param use  the new flag value.
541     * 
542     * @since 1.0.5
543     * 
544     * @see #getUseOutlinePaint()
545     */
546    public void setUseOutlinePaint(boolean use) {
547        if (this.useOutlinePaint != use) {
548            this.useOutlinePaint = use;
549            fireChangeEvent();
550        }
551    }
552    
553    /**
554     * Initialises the renderer then returns the number of 'passes' through the
555     * data that the renderer will require (usually just one).  This method 
556     * will be called before the first item is rendered, giving the renderer 
557     * an opportunity to initialise any state information it wants to maintain.
558     * The renderer can do nothing if it chooses.
559     *
560     * @param g2  the graphics device.
561     * @param dataArea  the area inside the axes.
562     * @param plot  the plot.
563     * @param dataset  the data.
564     * @param info  an optional info collection object to return data back to 
565     *              the caller.
566     *
567     * @return The number of passes the renderer requires.
568     */
569    public XYItemRendererState initialise(Graphics2D g2,
570                                          Rectangle2D dataArea,
571                                          XYPlot plot,
572                                          XYDataset dataset,
573                                          PlotRenderingInfo info) {
574          
575        // calculate the maximum allowed candle width from the axis...
576        ValueAxis axis = plot.getDomainAxis();
577        double x1 = axis.getLowerBound();
578        double x2 = x1 + this.maxCandleWidthInMilliseconds;
579        RectangleEdge edge = plot.getDomainAxisEdge();
580        double xx1 = axis.valueToJava2D(x1, dataArea, edge);
581        double xx2 = axis.valueToJava2D(x2, dataArea, edge);
582        this.maxCandleWidth = Math.abs(xx2 - xx1); 
583            // Absolute value, since the relative x 
584            // positions are reversed for horizontal orientation
585        
586        // calculate the highest volume in the dataset... 
587        if (this.drawVolume) {
588            OHLCDataset highLowDataset = (OHLCDataset) dataset;
589            this.maxVolume = 0.0;
590            for (int series = 0; series < highLowDataset.getSeriesCount(); 
591                 series++) {
592                for (int item = 0; item < highLowDataset.getItemCount(series); 
593                     item++) {
594                    double volume = highLowDataset.getVolumeValue(series, item);
595                    if (volume > this.maxVolume) {
596                        this.maxVolume = volume;
597                    }
598                    
599                }    
600            }
601        }
602        
603        return new XYItemRendererState(info);
604    }
605
606    /**
607     * Draws the visual representation of a single data item.
608     *
609     * @param g2  the graphics device.
610     * @param state  the renderer state.
611     * @param dataArea  the area within which the plot is being drawn.
612     * @param info  collects info about the drawing.
613     * @param plot  the plot (can be used to obtain standard color 
614     *              information etc).
615     * @param domainAxis  the domain axis.
616     * @param rangeAxis  the range axis.
617     * @param dataset  the dataset.
618     * @param series  the series index (zero-based).
619     * @param item  the item index (zero-based).
620     * @param crosshairState  crosshair information for the plot 
621     *                        (<code>null</code> permitted).
622     * @param pass  the pass index.
623     */
624    public void drawItem(Graphics2D g2, 
625                         XYItemRendererState state,
626                         Rectangle2D dataArea,
627                         PlotRenderingInfo info,
628                         XYPlot plot, 
629                         ValueAxis domainAxis, 
630                         ValueAxis rangeAxis,
631                         XYDataset dataset, 
632                         int series, 
633                         int item,
634                         CrosshairState crosshairState,
635                         int pass) {
636
637        boolean horiz;
638        PlotOrientation orientation = plot.getOrientation();
639        if (orientation == PlotOrientation.HORIZONTAL) {
640            horiz = true;
641        }
642        else if (orientation == PlotOrientation.VERTICAL) {
643            horiz = false;
644        }
645        else {
646            return;
647        }
648        
649        // setup for collecting optional entity info...
650        EntityCollection entities = null;
651        if (info != null) {
652            entities = info.getOwner().getEntityCollection();
653        }
654
655        OHLCDataset highLowData = (OHLCDataset) dataset;
656
657        double x = highLowData.getXValue(series, item);
658        double yHigh = highLowData.getHighValue(series, item);
659        double yLow = highLowData.getLowValue(series, item);
660        double yOpen = highLowData.getOpenValue(series, item);
661        double yClose = highLowData.getCloseValue(series, item);
662
663        RectangleEdge domainEdge = plot.getDomainAxisEdge();
664        double xx = domainAxis.valueToJava2D(x, dataArea, domainEdge);
665
666        RectangleEdge edge = plot.getRangeAxisEdge();
667        double yyHigh = rangeAxis.valueToJava2D(yHigh, dataArea, edge);
668        double yyLow = rangeAxis.valueToJava2D(yLow, dataArea, edge);
669        double yyOpen = rangeAxis.valueToJava2D(yOpen, dataArea, edge);
670        double yyClose = rangeAxis.valueToJava2D(yClose, dataArea, edge);
671
672        double volumeWidth;
673        double stickWidth;
674        if (this.candleWidth > 0) {
675            // These are deliberately not bounded to minimums/maxCandleWidth to
676            //  retain old behaviour.
677            volumeWidth = this.candleWidth;
678            stickWidth = this.candleWidth;
679        }
680        else {
681            double xxWidth = 0;
682            int itemCount;
683            switch (this.autoWidthMethod) {
684            
685                case WIDTHMETHOD_AVERAGE:
686                    itemCount = highLowData.getItemCount(series);
687                    if (horiz) {
688                        xxWidth = dataArea.getHeight() / itemCount;
689                    }
690                    else {
691                        xxWidth = dataArea.getWidth() / itemCount;
692                    }
693                    break;
694            
695                case WIDTHMETHOD_SMALLEST:
696                    // Note: It would be nice to pre-calculate this per series
697                    itemCount = highLowData.getItemCount(series);
698                    double lastPos = -1;
699                    xxWidth = dataArea.getWidth();
700                    for (int i = 0; i < itemCount; i++) {
701                        double pos = domainAxis.valueToJava2D(
702                                highLowData.getXValue(series, i), dataArea, 
703                                domainEdge);
704                        if (lastPos != -1) {
705                            xxWidth = Math.min(xxWidth, 
706                                    Math.abs(pos - lastPos));
707                        }
708                        lastPos = pos;
709                    }
710                    break;
711            
712                case WIDTHMETHOD_INTERVALDATA:
713                    IntervalXYDataset intervalXYData 
714                            = (IntervalXYDataset) dataset;
715                    double startPos = domainAxis.valueToJava2D(
716                            intervalXYData.getStartXValue(series, item), 
717                            dataArea, plot.getDomainAxisEdge());
718                    double endPos = domainAxis.valueToJava2D(
719                            intervalXYData.getEndXValue(series, item), 
720                            dataArea, plot.getDomainAxisEdge());
721                    xxWidth = Math.abs(endPos - startPos);
722                    break;
723                
724            }
725            xxWidth -= 2 * this.autoWidthGap;
726            xxWidth *= this.autoWidthFactor;
727            xxWidth = Math.min(xxWidth, this.maxCandleWidth);
728            volumeWidth = Math.max(Math.min(1, this.maxCandleWidth), xxWidth);
729            stickWidth = Math.max(Math.min(3, this.maxCandleWidth), xxWidth);
730        }
731
732        Paint p = getItemPaint(series, item);
733        Paint outlinePaint = null;
734        if (this.useOutlinePaint) {
735            outlinePaint = getItemOutlinePaint(series, item);
736        }
737        Stroke s = getItemStroke(series, item);
738
739        g2.setStroke(s);
740
741        if (this.drawVolume) {
742            int volume = (int) highLowData.getVolumeValue(series, item);
743            double volumeHeight = volume / this.maxVolume;
744
745            double min, max;
746            if (horiz) {
747                min = dataArea.getMinX();
748                max = dataArea.getMaxX();
749            }
750            else {
751                min = dataArea.getMinY();
752                max = dataArea.getMaxY();
753            }
754
755            double zzVolume = volumeHeight * (max - min);
756
757            g2.setPaint(getVolumePaint());
758            Composite originalComposite = g2.getComposite();
759            g2.setComposite(
760                AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f)
761            );
762
763            if (horiz) {
764                g2.fill(new Rectangle2D.Double(min, xx - volumeWidth / 2,
765                        zzVolume, volumeWidth));
766            }
767            else {
768                g2.fill(new Rectangle2D.Double(xx - volumeWidth / 2,
769                        max - zzVolume, volumeWidth, zzVolume));
770            }
771
772            g2.setComposite(originalComposite);
773        }
774
775        if (this.useOutlinePaint) {
776            g2.setPaint(outlinePaint);
777        }
778        else {
779            g2.setPaint(p);
780        }
781
782        double yyMaxOpenClose = Math.max(yyOpen, yyClose);
783        double yyMinOpenClose = Math.min(yyOpen, yyClose);
784        double maxOpenClose = Math.max(yOpen, yClose);
785        double minOpenClose = Math.min(yOpen, yClose);
786
787        // draw the upper shadow
788        if (yHigh > maxOpenClose) {
789            if (horiz) {
790                g2.draw(new Line2D.Double(yyHigh, xx, yyMaxOpenClose, xx));
791            }
792            else {
793                g2.draw(new Line2D.Double(xx, yyHigh, xx, yyMaxOpenClose));
794            }
795        }
796
797        // draw the lower shadow
798        if (yLow < minOpenClose) {
799            if (horiz) {
800                g2.draw(new Line2D.Double(yyLow, xx, yyMinOpenClose, xx));
801            }
802            else {
803                g2.draw(new Line2D.Double(xx, yyLow, xx, yyMinOpenClose));
804            }
805        }
806
807        // draw the body
808        Shape body = null;
809        if (horiz) {
810            body = new Rectangle2D.Double(yyMinOpenClose, xx - stickWidth / 2, 
811                    yyMaxOpenClose - yyMinOpenClose, stickWidth);
812        } 
813        else {
814            body = new Rectangle2D.Double(xx - stickWidth / 2, yyMinOpenClose,
815                    stickWidth, yyMaxOpenClose - yyMinOpenClose);
816        }
817        if (yClose > yOpen) {
818            if (this.upPaint != null) {
819                g2.setPaint(this.upPaint);
820            }
821            else {
822                g2.setPaint(p);
823            }
824            g2.fill(body);
825        }
826        else {
827            if (this.downPaint != null) {
828                g2.setPaint(this.downPaint);
829            }
830            else {
831                g2.setPaint(p);
832            }
833            g2.fill(body);
834        }
835        if (this.useOutlinePaint) {
836            g2.setPaint(outlinePaint);
837        }
838        else {
839            g2.setPaint(p);
840        }
841        g2.draw(body);
842
843        // add an entity for the item...
844        if (entities != null) {
845            String tip = null;
846            XYToolTipGenerator generator = getToolTipGenerator(series, item);
847            if (generator != null) {
848                tip = generator.generateToolTip(dataset, series, item);
849            }
850            String url = null;
851            if (getURLGenerator() != null) {
852                url = getURLGenerator().generateURL(dataset, series, item);
853            }
854            XYItemEntity entity = new XYItemEntity(body, dataset, series, item, 
855                    tip, url);
856            entities.add(entity);
857        }
858
859    }
860
861    /**
862     * Tests this renderer for equality with another object.
863     *
864     * @param obj  the object (<code>null</code> permitted).
865     *
866     * @return <code>true</code> or <code>false</code>.
867     */
868    public boolean equals(Object obj) {
869        if (obj == this) {
870            return true;
871        }
872        if (!(obj instanceof CandlestickRenderer)) {
873            return false;
874        }
875        CandlestickRenderer that = (CandlestickRenderer) obj;
876        if (this.candleWidth != that.candleWidth) {
877            return false;
878        }
879        if (!PaintUtilities.equal(this.upPaint, that.upPaint)) {
880            return false;
881        }
882        if (!PaintUtilities.equal(this.downPaint, that.downPaint)) {
883            return false;
884        }
885        if (this.drawVolume != that.drawVolume) {
886            return false;
887        }
888        if (this.maxCandleWidthInMilliseconds 
889                != that.maxCandleWidthInMilliseconds) {
890            return false;
891        }
892        if (this.autoWidthMethod != that.autoWidthMethod) {
893            return false;
894        }
895        if (this.autoWidthFactor != that.autoWidthFactor) {
896            return false;
897        }
898        if (this.autoWidthGap != that.autoWidthGap) {
899            return false;
900        }
901        if (this.useOutlinePaint != that.useOutlinePaint) {
902            return false;
903        }
904        if (!PaintUtilities.equal(this.volumePaint, that.volumePaint)) {
905            return false;
906        }
907        return super.equals(obj);
908    }
909
910    /**
911     * Returns a clone of the renderer.
912     * 
913     * @return A clone.
914     * 
915     * @throws CloneNotSupportedException  if the renderer cannot be cloned.
916     */
917    public Object clone() throws CloneNotSupportedException {
918        return super.clone();
919    }
920
921    /**
922     * Provides serialization support.
923     *
924     * @param stream  the output stream.
925     *
926     * @throws IOException  if there is an I/O error.
927     */
928    private void writeObject(ObjectOutputStream stream) throws IOException {
929        stream.defaultWriteObject();
930        SerialUtilities.writePaint(this.upPaint, stream);
931        SerialUtilities.writePaint(this.downPaint, stream);
932        SerialUtilities.writePaint(this.volumePaint, stream);
933    }
934
935    /**
936     * Provides serialization support.
937     *
938     * @param stream  the input stream.
939     *
940     * @throws IOException  if there is an I/O error.
941     * @throws ClassNotFoundException  if there is a classpath problem.
942     */
943    private void readObject(ObjectInputStream stream) 
944            throws IOException, ClassNotFoundException {
945        stream.defaultReadObject();
946        this.upPaint = SerialUtilities.readPaint(stream);
947        this.downPaint = SerialUtilities.readPaint(stream);
948        this.volumePaint = SerialUtilities.readPaint(stream);
949    }
950
951    // --- DEPRECATED CODE ----------------------------------------------------
952    
953    /**
954     * Returns a flag indicating whether or not volume bars are drawn on the
955     * chart.
956     *
957     * @return <code>true</code> if volume bars are drawn on the chart.
958     * 
959     * @deprecated As of 1.0.5, you should use the {@link #getDrawVolume()} 
960     *         method.
961     */
962    public boolean drawVolume() {
963        return this.drawVolume;
964    }
965
966}