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 * HighLowRenderer.java
029 * --------------------
030 * (C) Copyright 2001-2016, by Object Refinery Limited.
031 *
032 * Original Author:  David Gilbert (for Object Refinery Limited);
033 * Contributor(s):   Richard Atkinson;
034 *                   Christian W. Zuckschwerdt;
035 *
036 */
037
038package org.jfree.chart.renderer.xy;
039
040import java.awt.Graphics2D;
041import java.awt.Paint;
042import java.awt.Shape;
043import java.awt.Stroke;
044import java.awt.geom.Line2D;
045import java.awt.geom.Rectangle2D;
046import java.io.IOException;
047import java.io.ObjectInputStream;
048import java.io.ObjectOutputStream;
049import java.io.Serializable;
050
051import org.jfree.chart.axis.ValueAxis;
052import org.jfree.chart.entity.EntityCollection;
053import org.jfree.chart.event.RendererChangeEvent;
054import org.jfree.chart.plot.CrosshairState;
055import org.jfree.chart.plot.PlotOrientation;
056import org.jfree.chart.plot.PlotRenderingInfo;
057import org.jfree.chart.plot.XYPlot;
058import org.jfree.chart.ui.RectangleEdge;
059import org.jfree.chart.util.PaintUtils;
060import org.jfree.chart.util.PublicCloneable;
061import org.jfree.chart.util.SerialUtils;
062import org.jfree.data.Range;
063import org.jfree.data.general.DatasetUtils;
064import org.jfree.data.xy.OHLCDataset;
065import org.jfree.data.xy.XYDataset;
066
067/**
068 * A renderer that draws high/low/open/close markers on an {@link XYPlot}
069 * (requires a {@link OHLCDataset}).  This renderer does not include code to
070 * calculate the crosshair point for the plot.
071 *
072 * The example shown here is generated by the {@code HighLowChartDemo1.java} 
073 * program included in the JFreeChart Demo Collection:
074 * <br><br>
075 * <img src="doc-files/HighLowRendererSample.png" alt="HighLowRendererSample.png">
076 */
077public class HighLowRenderer extends AbstractXYItemRenderer
078        implements XYItemRenderer, Cloneable, PublicCloneable, Serializable {
079
080    /** For serialization. */
081    private static final long serialVersionUID = -8135673815876552516L;
082
083    /** A flag that controls whether the open ticks are drawn. */
084    private boolean drawOpenTicks;
085
086    /** A flag that controls whether the close ticks are drawn. */
087    private boolean drawCloseTicks;
088
089    /**
090     * The paint used for the open ticks (if {@code null}, the series
091     * paint is used instead).
092     */
093    private transient Paint openTickPaint;
094
095    /**
096     * The paint used for the close ticks (if {@code null}, the series
097     * paint is used instead).
098     */
099    private transient Paint closeTickPaint;
100
101    /**
102     * The tick length (in Java2D units).
103     *
104     * @since 1.0.10
105     */
106    private double tickLength;
107
108    /**
109     * The default constructor.
110     */
111    public HighLowRenderer() {
112        super();
113        this.drawOpenTicks = true;
114        this.drawCloseTicks = true;
115        this.tickLength = 2.0;
116    }
117
118    /**
119     * Returns the flag that controls whether open ticks are drawn.
120     *
121     * @return A boolean.
122     *
123     * @see #getDrawCloseTicks()
124     * @see #setDrawOpenTicks(boolean)
125     */
126    public boolean getDrawOpenTicks() {
127        return this.drawOpenTicks;
128    }
129
130    /**
131     * Sets the flag that controls whether open ticks are drawn, and sends a
132     * {@link RendererChangeEvent} to all registered listeners.
133     *
134     * @param draw  the flag.
135     *
136     * @see #getDrawOpenTicks()
137     */
138    public void setDrawOpenTicks(boolean draw) {
139        this.drawOpenTicks = draw;
140        fireChangeEvent();
141    }
142
143    /**
144     * Returns the flag that controls whether close ticks are drawn.
145     *
146     * @return A boolean.
147     *
148     * @see #getDrawOpenTicks()
149     * @see #setDrawCloseTicks(boolean)
150     */
151    public boolean getDrawCloseTicks() {
152        return this.drawCloseTicks;
153    }
154
155    /**
156     * Sets the flag that controls whether close ticks are drawn, and sends a
157     * {@link RendererChangeEvent} to all registered listeners.
158     *
159     * @param draw  the flag.
160     *
161     * @see #getDrawCloseTicks()
162     */
163    public void setDrawCloseTicks(boolean draw) {
164        this.drawCloseTicks = draw;
165        fireChangeEvent();
166    }
167
168    /**
169     * Returns the paint used to draw the ticks for the open values.
170     *
171     * @return The paint used to draw the ticks for the open values (possibly
172     *         {@code null}).
173     *
174     * @see #setOpenTickPaint(Paint)
175     */
176    public Paint getOpenTickPaint() {
177        return this.openTickPaint;
178    }
179
180    /**
181     * Sets the paint used to draw the ticks for the open values and sends a
182     * {@link RendererChangeEvent} to all registered listeners.  If you set
183     * this to {@code null} (the default), the series paint is used
184     * instead.
185     *
186     * @param paint  the paint ({@code null} permitted).
187     *
188     * @see #getOpenTickPaint()
189     */
190    public void setOpenTickPaint(Paint paint) {
191        this.openTickPaint = paint;
192        fireChangeEvent();
193    }
194
195    /**
196     * Returns the paint used to draw the ticks for the close values.
197     *
198     * @return The paint used to draw the ticks for the close values (possibly
199     *         {@code null}).
200     *
201     * @see #setCloseTickPaint(Paint)
202     */
203    public Paint getCloseTickPaint() {
204        return this.closeTickPaint;
205    }
206
207    /**
208     * Sets the paint used to draw the ticks for the close values and sends a
209     * {@link RendererChangeEvent} to all registered listeners.  If you set
210     * this to {@code null} (the default), the series paint is used
211     * instead.
212     *
213     * @param paint  the paint ({@code null} permitted).
214     *
215     * @see #getCloseTickPaint()
216     */
217    public void setCloseTickPaint(Paint paint) {
218        this.closeTickPaint = paint;
219        fireChangeEvent();
220    }
221
222    /**
223     * Returns the tick length (in Java2D units).
224     *
225     * @return The tick length.
226     *
227     * @since 1.0.10
228     *
229     * @see #setTickLength(double)
230     */
231    public double getTickLength() {
232        return this.tickLength;
233    }
234
235    /**
236     * Sets the tick length (in Java2D units) and sends a
237     * {@link RendererChangeEvent} to all registered listeners.
238     *
239     * @param length  the length.
240     *
241     * @since 1.0.10
242     *
243     * @see #getTickLength()
244     */
245    public void setTickLength(double length) {
246        this.tickLength = length;
247        fireChangeEvent();
248    }
249
250    /**
251     * Returns the range of values the renderer requires to display all the
252     * items from the specified dataset.
253     *
254     * @param dataset  the dataset ({@code null} permitted).
255     *
256     * @return The range ({@code null} if the dataset is {@code null}
257     *         or empty).
258     */
259    @Override
260    public Range findRangeBounds(XYDataset dataset) {
261        if (dataset != null) {
262            return DatasetUtils.findRangeBounds(dataset, true);
263        }
264        else {
265            return null;
266        }
267    }
268
269    /**
270     * Draws the visual representation of a single data item.
271     *
272     * @param g2  the graphics device.
273     * @param state  the renderer state.
274     * @param dataArea  the area within which the plot is being drawn.
275     * @param info  collects information about the drawing.
276     * @param plot  the plot (can be used to obtain standard color
277     *              information etc).
278     * @param domainAxis  the domain axis.
279     * @param rangeAxis  the range axis.
280     * @param dataset  the dataset.
281     * @param series  the series index (zero-based).
282     * @param item  the item index (zero-based).
283     * @param crosshairState  crosshair information for the plot
284     *                        ({@code null} permitted).
285     * @param pass  the pass index.
286     */
287    @Override
288    public void drawItem(Graphics2D g2, XYItemRendererState state,
289            Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
290            ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset,
291            int series, int item, CrosshairState crosshairState, int pass) {
292
293        double x = dataset.getXValue(series, item);
294        if (!domainAxis.getRange().contains(x)) {
295            return;    // the x value is not within the axis range
296        }
297        double xx = domainAxis.valueToJava2D(x, dataArea,
298                plot.getDomainAxisEdge());
299
300        // setup for collecting optional entity info...
301        Shape entityArea = null;
302        EntityCollection entities = null;
303        if (info != null) {
304            entities = info.getOwner().getEntityCollection();
305        }
306
307        PlotOrientation orientation = plot.getOrientation();
308        RectangleEdge location = plot.getRangeAxisEdge();
309
310        Paint itemPaint = getItemPaint(series, item);
311        Stroke itemStroke = getItemStroke(series, item);
312        g2.setPaint(itemPaint);
313        g2.setStroke(itemStroke);
314
315        if (dataset instanceof OHLCDataset) {
316            OHLCDataset hld = (OHLCDataset) dataset;
317
318            double yHigh = hld.getHighValue(series, item);
319            double yLow = hld.getLowValue(series, item);
320            if (!Double.isNaN(yHigh) && !Double.isNaN(yLow)) {
321                double yyHigh = rangeAxis.valueToJava2D(yHigh, dataArea,
322                        location);
323                double yyLow = rangeAxis.valueToJava2D(yLow, dataArea,
324                        location);
325                if (orientation == PlotOrientation.HORIZONTAL) {
326                    g2.draw(new Line2D.Double(yyLow, xx, yyHigh, xx));
327                    entityArea = new Rectangle2D.Double(Math.min(yyLow, yyHigh),
328                            xx - 1.0, Math.abs(yyHigh - yyLow), 2.0);
329                }
330                else if (orientation == PlotOrientation.VERTICAL) {
331                    g2.draw(new Line2D.Double(xx, yyLow, xx, yyHigh));
332                    entityArea = new Rectangle2D.Double(xx - 1.0,
333                            Math.min(yyLow, yyHigh), 2.0,
334                            Math.abs(yyHigh - yyLow));
335                }
336            }
337
338            double delta = getTickLength();
339            if (domainAxis.isInverted()) {
340                delta = -delta;
341            }
342            if (getDrawOpenTicks()) {
343                double yOpen = hld.getOpenValue(series, item);
344                if (!Double.isNaN(yOpen)) {
345                    double yyOpen = rangeAxis.valueToJava2D(yOpen, dataArea,
346                            location);
347                    if (this.openTickPaint != null) {
348                        g2.setPaint(this.openTickPaint);
349                    }
350                    else {
351                        g2.setPaint(itemPaint);
352                    }
353                    if (orientation == PlotOrientation.HORIZONTAL) {
354                        g2.draw(new Line2D.Double(yyOpen, xx + delta, yyOpen,
355                                xx));
356                    }
357                    else if (orientation == PlotOrientation.VERTICAL) {
358                        g2.draw(new Line2D.Double(xx - delta, yyOpen, xx,
359                                yyOpen));
360                    }
361                }
362            }
363
364            if (getDrawCloseTicks()) {
365                double yClose = hld.getCloseValue(series, item);
366                if (!Double.isNaN(yClose)) {
367                    double yyClose = rangeAxis.valueToJava2D(
368                        yClose, dataArea, location);
369                    if (this.closeTickPaint != null) {
370                        g2.setPaint(this.closeTickPaint);
371                    }
372                    else {
373                        g2.setPaint(itemPaint);
374                    }
375                    if (orientation == PlotOrientation.HORIZONTAL) {
376                        g2.draw(new Line2D.Double(yyClose, xx, yyClose,
377                                xx - delta));
378                    }
379                    else if (orientation == PlotOrientation.VERTICAL) {
380                        g2.draw(new Line2D.Double(xx, yyClose, xx + delta,
381                                yyClose));
382                    }
383                }
384            }
385
386        }
387        else {
388            // not a HighLowDataset, so just draw a line connecting this point
389            // with the previous point...
390            if (item > 0) {
391                double x0 = dataset.getXValue(series, item - 1);
392                double y0 = dataset.getYValue(series, item - 1);
393                double y = dataset.getYValue(series, item);
394                if (Double.isNaN(x0) || Double.isNaN(y0) || Double.isNaN(y)) {
395                    return;
396                }
397                double xx0 = domainAxis.valueToJava2D(x0, dataArea,
398                        plot.getDomainAxisEdge());
399                double yy0 = rangeAxis.valueToJava2D(y0, dataArea, location);
400                double yy = rangeAxis.valueToJava2D(y, dataArea, location);
401                if (orientation == PlotOrientation.HORIZONTAL) {
402                    g2.draw(new Line2D.Double(yy0, xx0, yy, xx));
403                }
404                else if (orientation == PlotOrientation.VERTICAL) {
405                    g2.draw(new Line2D.Double(xx0, yy0, xx, yy));
406                }
407            }
408        }
409
410        if (entities != null) {
411            addEntity(entities, entityArea, dataset, series, item, 0.0, 0.0);
412        }
413
414    }
415
416    /**
417     * Returns a clone of the renderer.
418     *
419     * @return A clone.
420     *
421     * @throws CloneNotSupportedException  if the renderer cannot be cloned.
422     */
423    @Override
424    public Object clone() throws CloneNotSupportedException {
425        return super.clone();
426    }
427
428    /**
429     * Tests this renderer for equality with an arbitrary object.
430     *
431     * @param obj  the object ({@code null} permitted).
432     *
433     * @return A boolean.
434     */
435    @Override
436    public boolean equals(Object obj) {
437        if (this == obj) {
438            return true;
439        }
440        if (!(obj instanceof HighLowRenderer)) {
441            return false;
442        }
443        HighLowRenderer that = (HighLowRenderer) obj;
444        if (this.drawOpenTicks != that.drawOpenTicks) {
445            return false;
446        }
447        if (this.drawCloseTicks != that.drawCloseTicks) {
448            return false;
449        }
450        if (!PaintUtils.equal(this.openTickPaint, that.openTickPaint)) {
451            return false;
452        }
453        if (!PaintUtils.equal(this.closeTickPaint, that.closeTickPaint)) {
454            return false;
455        }
456        if (this.tickLength != that.tickLength) {
457            return false;
458        }
459        if (!super.equals(obj)) {
460            return false;
461        }
462        return true;
463    }
464
465    /**
466     * Provides serialization support.
467     *
468     * @param stream  the input stream.
469     *
470     * @throws IOException  if there is an I/O error.
471     * @throws ClassNotFoundException  if there is a classpath problem.
472     */
473    private void readObject(ObjectInputStream stream)
474            throws IOException, ClassNotFoundException {
475        stream.defaultReadObject();
476        this.openTickPaint = SerialUtils.readPaint(stream);
477        this.closeTickPaint = SerialUtils.readPaint(stream);
478    }
479
480    /**
481     * Provides serialization support.
482     *
483     * @param stream  the output stream.
484     *
485     * @throws IOException  if there is an I/O error.
486     */
487    private void writeObject(ObjectOutputStream stream) throws IOException {
488        stream.defaultWriteObject();
489        SerialUtils.writePaint(this.openTickPaint, stream);
490        SerialUtils.writePaint(this.closeTickPaint, stream);
491    }
492
493}