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