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