001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it 
010 * under the terms of the GNU Lesser General Public License as published by 
011 * the Free Software Foundation; either version 2.1 of the License, or 
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but 
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
022 * USA.  
023 *
024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
025 * in the United States and other countries.]
026 *
027 * --------------------
028 * HighLowRenderer.java
029 * --------------------
030 * (C) Copyright 2001-2007, 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 * Changes
037 * -------
038 * 13-Dec-2001 : Version 1 (DG);
039 * 23-Jan-2002 : Added DrawInfo parameter to drawItem() method (DG);
040 * 28-Mar-2002 : Added a property change listener mechanism so that renderers 
041 *               no longer need to be immutable (DG);
042 * 09-Apr-2002 : Removed translatedRangeZero from the drawItem() method, and 
043 *               changed the return type of the drawItem method to void, 
044 *               reflecting a change in the XYItemRenderer interface.  Added 
045 *               tooltip code to drawItem() method (DG);
046 * 05-Aug-2002 : Small modification to drawItem method to support URLs for 
047 *               HTML image maps (RA);
048 * 25-Mar-2003 : Implemented Serializable (DG);
049 * 01-May-2003 : Modified drawItem() method signature (DG);
050 * 30-Jul-2003 : Modified entity constructor (CZ);
051 * 31-Jul-2003 : Deprecated constructor (DG);
052 * 20-Aug-2003 : Implemented Cloneable and PublicCloneable (DG);
053 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
054 * 29-Jan-2004 : Fixed bug (882392) when rendering with 
055 *               PlotOrientation.HORIZONTAL (DG);
056 * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState.  Renamed 
057 *               XYToolTipGenerator --> XYItemLabelGenerator (DG);
058 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 
059 *               getYValue() (DG);
060 * 01-Nov-2005 : Added optional openTickPaint and closeTickPaint settings (DG);
061 * ------------- JFREECHART 1.0.0 ---------------------------------------------
062 * 06-Jul-2006 : Replace dataset methods getX() --> getXValue() (DG);
063 * 
064 */
065
066package org.jfree.chart.renderer.xy;
067
068import java.awt.Graphics2D;
069import java.awt.Paint;
070import java.awt.Shape;
071import java.awt.Stroke;
072import java.awt.geom.Line2D;
073import java.awt.geom.Rectangle2D;
074import java.io.IOException;
075import java.io.ObjectInputStream;
076import java.io.ObjectOutputStream;
077import java.io.Serializable;
078
079import org.jfree.chart.axis.ValueAxis;
080import org.jfree.chart.entity.EntityCollection;
081import org.jfree.chart.entity.XYItemEntity;
082import org.jfree.chart.event.RendererChangeEvent;
083import org.jfree.chart.labels.XYToolTipGenerator;
084import org.jfree.chart.plot.CrosshairState;
085import org.jfree.chart.plot.PlotOrientation;
086import org.jfree.chart.plot.PlotRenderingInfo;
087import org.jfree.chart.plot.XYPlot;
088import org.jfree.data.xy.OHLCDataset;
089import org.jfree.data.xy.XYDataset;
090import org.jfree.io.SerialUtilities;
091import org.jfree.ui.RectangleEdge;
092import org.jfree.util.PaintUtilities;
093import org.jfree.util.PublicCloneable;
094
095/**
096 * A renderer that draws high/low/open/close markers on an {@link XYPlot} 
097 * (requires a {@link OHLCDataset}).  This renderer does not include code to 
098 * calculate the crosshair point for the plot.
099 */
100public class HighLowRenderer extends AbstractXYItemRenderer
101                             implements XYItemRenderer,
102                                        Cloneable,
103                                        PublicCloneable,
104                                        Serializable {
105    
106    /** For serialization. */
107    private static final long serialVersionUID = -8135673815876552516L;
108    
109    /** A flag that controls whether the open ticks are drawn. */
110    private boolean drawOpenTicks;
111
112    /** A flag that controls whether the close ticks are drawn. */
113    private boolean drawCloseTicks;
114    
115    /** 
116     * The paint used for the open ticks (if <code>null</code>, the series
117     * paint is used instead).
118     */
119    private transient Paint openTickPaint;
120    
121    /** 
122     * The paint used for the close ticks (if <code>null</code>, the series
123     * paint is used instead).
124     */
125    private transient Paint closeTickPaint;
126
127    /**
128     * The default constructor.
129     */
130    public HighLowRenderer() {
131        super();
132        this.drawOpenTicks = true;
133        this.drawCloseTicks = true;
134    }
135
136    /**
137     * Returns the flag that controls whether open ticks are drawn.
138     * 
139     * @return A boolean.
140     */
141    public boolean getDrawOpenTicks() {
142        return this.drawOpenTicks;
143    }
144    
145    /**
146     * Sets the flag that controls whether open ticks are drawn, and sends a 
147     * {@link RendererChangeEvent} to all registered listeners.
148     * 
149     * @param draw  the flag.
150     */
151    public void setDrawOpenTicks(boolean draw) {
152        this.drawOpenTicks = draw;
153        notifyListeners(new RendererChangeEvent(this));
154    }
155    
156    /**
157     * Returns the flag that controls whether close ticks are drawn.
158     * 
159     * @return A boolean.
160     */
161    public boolean getDrawCloseTicks() {
162        return this.drawCloseTicks;
163    }
164    
165    /**
166     * Sets the flag that controls whether close ticks are drawn, and sends a 
167     * {@link RendererChangeEvent} to all registered listeners.
168     * 
169     * @param draw  the flag.
170     */
171    public void setDrawCloseTicks(boolean draw) {
172        this.drawCloseTicks = draw;
173        notifyListeners(new RendererChangeEvent(this));
174    }
175    
176    /**
177     * Returns the paint used to draw the ticks for the open values.
178     * 
179     * @return The paint used to draw the ticks for the open values (possibly 
180     *         <code>null</code>).
181     */
182    public Paint getOpenTickPaint() {
183        return this.openTickPaint;
184    }
185    
186    /**
187     * Sets the paint used to draw the ticks for the open values and sends a 
188     * {@link RendererChangeEvent} to all registered listeners.  If you set
189     * this to <code>null</code> (the default), the series paint is used 
190     * instead.
191     * 
192     * @param paint  the paint (<code>null</code> permitted).
193     */
194    public void setOpenTickPaint(Paint paint) {
195        this.openTickPaint = paint;
196        notifyListeners(new RendererChangeEvent(this));
197    }
198    
199    /**
200     * Returns the paint used to draw the ticks for the close values.
201     * 
202     * @return The paint used to draw the ticks for the close values (possibly 
203     *         <code>null</code>).
204     */
205    public Paint getCloseTickPaint() {
206        return this.closeTickPaint;
207    }
208    
209    /**
210     * Sets the paint used to draw the ticks for the close values and sends a 
211     * {@link RendererChangeEvent} to all registered listeners.  If you set
212     * this to <code>null</code> (the default), the series paint is used 
213     * instead.
214     * 
215     * @param paint  the paint (<code>null</code> permitted).
216     */
217    public void setCloseTickPaint(Paint paint) {
218        this.closeTickPaint = paint;
219        notifyListeners(new RendererChangeEvent(this));
220    }
221    
222    /**
223     * Draws the visual representation of a single data item.
224     *
225     * @param g2  the graphics device.
226     * @param state  the renderer state.
227     * @param dataArea  the area within which the plot is being drawn.
228     * @param info  collects information about the drawing.
229     * @param plot  the plot (can be used to obtain standard color 
230     *              information etc).
231     * @param domainAxis  the domain axis.
232     * @param rangeAxis  the range axis.
233     * @param dataset  the dataset.
234     * @param series  the series index (zero-based).
235     * @param item  the item index (zero-based).
236     * @param crosshairState  crosshair information for the plot 
237     *                        (<code>null</code> permitted).
238     * @param pass  the pass index.
239     */
240    public void drawItem(Graphics2D g2,
241                         XYItemRendererState state,
242                         Rectangle2D dataArea,
243                         PlotRenderingInfo info,
244                         XYPlot plot,
245                         ValueAxis domainAxis,
246                         ValueAxis rangeAxis,
247                         XYDataset dataset,
248                         int series,
249                         int item,
250                         CrosshairState crosshairState,
251                         int pass) {
252
253        double x = dataset.getXValue(series, item);
254        if (!domainAxis.getRange().contains(x)) {
255            return;    // the x value is not within the axis range
256        }
257        double xx = domainAxis.valueToJava2D(x, dataArea, 
258                plot.getDomainAxisEdge());
259        
260        // setup for collecting optional entity info...
261        Shape entityArea = null;
262        EntityCollection entities = null;
263        if (info != null) {
264            entities = info.getOwner().getEntityCollection();
265        }
266
267        PlotOrientation orientation = plot.getOrientation();
268        RectangleEdge location = plot.getRangeAxisEdge();
269
270        Paint itemPaint = getItemPaint(series, item);
271        Stroke itemStroke = getItemStroke(series, item);
272        g2.setPaint(itemPaint);
273        g2.setStroke(itemStroke);
274        
275        if (dataset instanceof OHLCDataset) {
276            OHLCDataset hld = (OHLCDataset) dataset;
277            
278            double yHigh = hld.getHighValue(series, item);
279            double yLow = hld.getLowValue(series, item);
280            if (!Double.isNaN(yHigh) && !Double.isNaN(yLow)) {
281                double yyHigh = rangeAxis.valueToJava2D(yHigh, dataArea, 
282                        location);
283                double yyLow = rangeAxis.valueToJava2D(yLow, dataArea, 
284                        location);
285                if (orientation == PlotOrientation.HORIZONTAL) {
286                    g2.draw(new Line2D.Double(yyLow, xx, yyHigh, xx));
287                    entityArea = new Rectangle2D.Double(Math.min(yyLow, yyHigh),
288                            xx - 1.0, Math.abs(yyHigh - yyLow), 2.0);
289                }
290                else if (orientation == PlotOrientation.VERTICAL) {
291                    g2.draw(new Line2D.Double(xx, yyLow, xx, yyHigh));   
292                    entityArea = new Rectangle2D.Double(xx - 1.0, 
293                            Math.min(yyLow, yyHigh), 2.0,  
294                            Math.abs(yyHigh - yyLow));
295                }
296            }
297            
298            double delta = 2.0;
299            if (domainAxis.isInverted()) {
300                delta = -delta;
301            }
302            if (getDrawOpenTicks()) {
303                double yOpen = hld.getOpenValue(series, item);
304                if (!Double.isNaN(yOpen)) {
305                    double yyOpen = rangeAxis.valueToJava2D(yOpen, dataArea, 
306                            location);
307                    if (this.openTickPaint != null) {
308                        g2.setPaint(this.openTickPaint);
309                    }
310                    else {
311                        g2.setPaint(itemPaint);
312                    }
313                    if (orientation == PlotOrientation.HORIZONTAL) {
314                        g2.draw(new Line2D.Double(yyOpen, xx + delta, yyOpen, 
315                                xx));   
316                    }
317                    else if (orientation == PlotOrientation.VERTICAL) {
318                        g2.draw(new Line2D.Double(xx - delta, yyOpen, xx, 
319                                yyOpen));   
320                    }
321                }
322            }
323            
324            if (getDrawCloseTicks()) {
325                double yClose = hld.getCloseValue(series, item);
326                if (!Double.isNaN(yClose)) {
327                    double yyClose = rangeAxis.valueToJava2D(
328                        yClose, dataArea, location);
329                    if (this.closeTickPaint != null) {
330                        g2.setPaint(this.closeTickPaint);
331                    }
332                    else {
333                        g2.setPaint(itemPaint);
334                    }
335                    if (orientation == PlotOrientation.HORIZONTAL) {
336                        g2.draw(new Line2D.Double(yyClose, xx, yyClose, 
337                                xx - delta));   
338                    }
339                    else if (orientation == PlotOrientation.VERTICAL) {
340                        g2.draw(new Line2D.Double(xx, yyClose, xx + delta, 
341                                yyClose));   
342                    }
343                }
344            }
345  
346        }
347        else {
348            // not a HighLowDataset, so just draw a line connecting this point 
349            // with the previous point...
350            if (item > 0) {
351                double x0 = dataset.getXValue(series, item - 1);
352                double y0 = dataset.getYValue(series, item - 1);
353                double y = dataset.getYValue(series, item);
354                if (Double.isNaN(x0) || Double.isNaN(y0) || Double.isNaN(y)) {
355                    return;
356                }
357                double xx0 = domainAxis.valueToJava2D(x0, dataArea, 
358                        plot.getDomainAxisEdge());
359                double yy0 = rangeAxis.valueToJava2D(y0, dataArea, location);
360                double yy = rangeAxis.valueToJava2D(y, dataArea, location);
361                if (orientation == PlotOrientation.HORIZONTAL) {
362                    g2.draw(new Line2D.Double(yy0, xx0, yy, xx));
363                }
364                else if (orientation == PlotOrientation.VERTICAL) {
365                    g2.draw(new Line2D.Double(xx0, yy0, xx, yy));
366                }
367            }
368        }
369        
370        // add an entity for the item...
371        if (entities != null) {
372            String tip = null;
373            XYToolTipGenerator generator = getToolTipGenerator(series, item);
374            if (generator != null) {
375                tip = generator.generateToolTip(dataset, series, item);
376            }
377            String url = null;
378            if (getURLGenerator() != null) {
379                url = getURLGenerator().generateURL(dataset, series, item);
380            }
381            XYItemEntity entity = new XYItemEntity(entityArea, dataset, 
382                    series, item, tip, url);
383            entities.add(entity);
384        }
385
386    }
387    
388    /**
389     * Returns a clone of the renderer.
390     * 
391     * @return A clone.
392     * 
393     * @throws CloneNotSupportedException  if the renderer cannot be cloned.
394     */
395    public Object clone() throws CloneNotSupportedException {
396        return super.clone();
397    }
398    
399    /**
400     * Tests this renderer for equality with an arbitrary object.
401     * 
402     * @param obj  the object (<code>null</code> permitted).
403     * 
404     * @return A boolean.
405     */
406    public boolean equals(Object obj) {
407        if (this == obj) {
408            return true;
409        }
410        if (!(obj instanceof HighLowRenderer)) {
411            return false;
412        }
413        HighLowRenderer that = (HighLowRenderer) obj;
414        if (this.drawOpenTicks != that.drawOpenTicks) {
415            return false;
416        }
417        if (this.drawCloseTicks != that.drawCloseTicks) {
418            return false;
419        }
420        if (!PaintUtilities.equal(this.openTickPaint, that.openTickPaint)) {
421            return false;
422        }
423        if (!PaintUtilities.equal(this.closeTickPaint, that.closeTickPaint)) {
424            return false;
425        }
426        if (!super.equals(obj)) {
427            return false;
428        }
429        return true;
430    }
431    
432    /**
433     * Provides serialization support.
434     *
435     * @param stream  the input stream.
436     *
437     * @throws IOException  if there is an I/O error.
438     * @throws ClassNotFoundException  if there is a classpath problem.
439     */
440    private void readObject(ObjectInputStream stream) 
441            throws IOException, ClassNotFoundException {
442        stream.defaultReadObject();
443        this.openTickPaint = SerialUtilities.readPaint(stream);
444        this.closeTickPaint = SerialUtilities.readPaint(stream);
445    }
446    
447    /**
448     * Provides serialization support.
449     *
450     * @param stream  the output stream.
451     *
452     * @throws IOException  if there is an I/O error.
453     */
454    private void writeObject(ObjectOutputStream stream) throws IOException {
455        stream.defaultWriteObject();
456        SerialUtilities.writePaint(this.openTickPaint, stream);
457        SerialUtilities.writePaint(this.closeTickPaint, stream);
458    }
459
460}