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