001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2005, 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     * XYBarRenderer.java
029     * ------------------
030     * (C) Copyright 2001-2005, by Object Refinery Limited.
031     *
032     * Original Author:  David Gilbert (for Object Refinery Limited);
033     * Contributor(s):   Richard Atkinson;
034     *                   Christian W. Zuckschwerdt;
035     *                   Bill Kelemen;
036     *
037     * $Id: XYBarRenderer.java,v 1.14.2.4 2005/11/28 12:06:35 mungady Exp $
038     *
039     * Changes
040     * -------
041     * 13-Dec-2001 : Version 1, makes VerticalXYBarPlot class redundant (DG);
042     * 23-Jan-2002 : Added DrawInfo parameter to drawItem() method (DG);
043     * 09-Apr-2002 : Removed the translated zero from the drawItem method. Override 
044     *               the initialise() method to calculate it (DG);
045     * 24-May-2002 : Incorporated tooltips into chart entities (DG);
046     * 25-Jun-2002 : Removed redundant import (DG);
047     * 05-Aug-2002 : Small modification to drawItem method to support URLs for HTML 
048     *               image maps (RA);
049     * 25-Mar-2003 : Implemented Serializable (DG);
050     * 01-May-2003 : Modified drawItem() method signature (DG);
051     * 30-Jul-2003 : Modified entity constructor (CZ);
052     * 20-Aug-2003 : Implemented Cloneable and PublicCloneable (DG);
053     * 24-Aug-2003 : Added null checks in drawItem (BK);
054     * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
055     * 07-Oct-2003 : Added renderer state (DG);
056     * 05-Dec-2003 : Changed call to obtain outline paint (DG);
057     * 10-Feb-2004 : Added state class, updated drawItem() method to make 
058     *               cut-and-paste overriding easier, and replaced property change 
059     *               with RendererChangeEvent (DG);
060     * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState (DG);
061     * 26-Apr-2004 : Added gradient paint transformer (DG);
062     * 19-May-2004 : Fixed bug (879709) with bar zero value for secondary axis (DG);
063     * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 
064     *               getYValue() (DG);
065     * 01-Sep-2004 : Added a flag to control whether or not the bar outlines are 
066     *               drawn (DG);
067     * 03-Sep-2004 : Added option to use y-interval from dataset to determine the 
068     *               length of the bars (DG);
069     * 08-Sep-2004 : Added equals() method and updated clone() method (DG);
070     * 26-Jan-2005 : Added override for getLegendItem() method (DG);
071     * 20-Apr-2005 : Use generators for label tooltips and URLs (DG);
072     * 19-May-2005 : Added minimal item label implementation - needs improving (DG);
073     * 14-Oct-2005 : Fixed rendering problem with inverted axes (DG);
074     * 
075     */
076    
077    package org.jfree.chart.renderer.xy;
078    
079    import java.awt.GradientPaint;
080    import java.awt.Graphics2D;
081    import java.awt.Paint;
082    import java.awt.Shape;
083    import java.awt.Stroke;
084    import java.awt.geom.Rectangle2D;
085    import java.io.IOException;
086    import java.io.ObjectInputStream;
087    import java.io.ObjectOutputStream;
088    import java.io.Serializable;
089    
090    import org.jfree.chart.LegendItem;
091    import org.jfree.chart.axis.ValueAxis;
092    import org.jfree.chart.entity.EntityCollection;
093    import org.jfree.chart.entity.XYItemEntity;
094    import org.jfree.chart.event.RendererChangeEvent;
095    import org.jfree.chart.labels.XYSeriesLabelGenerator;
096    import org.jfree.chart.labels.XYToolTipGenerator;
097    import org.jfree.chart.plot.CrosshairState;
098    import org.jfree.chart.plot.PlotOrientation;
099    import org.jfree.chart.plot.PlotRenderingInfo;
100    import org.jfree.chart.plot.XYPlot;
101    import org.jfree.data.Range;
102    import org.jfree.data.general.DatasetUtilities;
103    import org.jfree.data.xy.IntervalXYDataset;
104    import org.jfree.data.xy.XYDataset;
105    import org.jfree.io.SerialUtilities;
106    import org.jfree.ui.GradientPaintTransformer;
107    import org.jfree.ui.RectangleEdge;
108    import org.jfree.ui.StandardGradientPaintTransformer;
109    import org.jfree.util.ObjectUtilities;
110    import org.jfree.util.PublicCloneable;
111    import org.jfree.util.ShapeUtilities;
112    
113    /**
114     * A renderer that draws bars on an {@link XYPlot} (requires an 
115     * {@link IntervalXYDataset}).
116     * <P>
117     * This renderer does not include any code for calculating the crosshair point.
118     */
119    public class XYBarRenderer extends AbstractXYItemRenderer 
120                               implements XYItemRenderer,
121                                          Cloneable,
122                                          PublicCloneable,
123                                          Serializable {
124        
125        /** For serialization. */
126        private static final long serialVersionUID = 770559577251370036L;
127    
128        /**
129         * The state class used by this renderer.
130         */
131        protected class XYBarRendererState extends XYItemRendererState {
132            
133            /** Base for bars against the range axis, in Java 2D space. */
134            private double g2Base;
135            
136            /**
137             * Creates a new state object.
138             * 
139             * @param info  the plot rendering info.
140             */
141            public XYBarRendererState(PlotRenderingInfo info) {
142                super(info);
143            }
144            
145            /**
146             * Returns the base (range) value in Java 2D space.
147             * 
148             * @return The base value.
149             */
150            public double getG2Base() {
151                return this.g2Base;
152            }
153            
154            /**
155             * Sets the range axis base in Java2D space.
156             * 
157             * @param value  the value.
158             */
159            public void setG2Base(double value) {
160                this.g2Base = value;
161            }
162        }
163    
164        /** The default base value for the bars. */
165        private double base;
166        
167        /** 
168         * A flag that controls whether the bars use the y-interval supplied by the 
169         * dataset. 
170         */
171        private boolean useYInterval;
172        
173        /** Percentage margin (to reduce the width of bars). */
174        private double margin;
175    
176        /** A flag that controls whether or not bar outlines are drawn. */
177        private boolean drawBarOutline;
178        
179        /** 
180         * An optional class used to transform gradient paint objects to fit each 
181         * bar. 
182         */
183        private GradientPaintTransformer gradientPaintTransformer; 
184        
185        /** 
186         * The shape used to represent a bar in each legend item (this should never
187         * be <code>null</code>). 
188         */
189        private transient Shape legendBar;
190        
191        /**
192         * The default constructor.
193         */
194        public XYBarRenderer() {
195            this(0.0);
196        }
197    
198        /**
199         * Constructs a new renderer.
200         *
201         * @param margin  the percentage amount to trim from the width of each bar.
202         */
203        public XYBarRenderer(double margin) {
204            super();
205            this.margin = margin;
206            this.base = 0.0;
207            this.useYInterval = false;
208            this.gradientPaintTransformer = new StandardGradientPaintTransformer(); 
209            this.drawBarOutline = true;
210            this.legendBar = new Rectangle2D.Double(-3.0, -5.0, 6.0, 10.0);
211        }
212        
213        /**
214         * Returns the base value for the bars.
215         * 
216         * @return The base value for the bars.
217         */
218        public double getBase() {
219            return this.base;    
220        }
221        
222        /**
223         * Sets the base value for the bars and sends a {@link RendererChangeEvent}
224         * to all registered listeners.  The base value is not used if the dataset's
225         * y-interval is being used to determine the bar length.
226         * 
227         * @param base  the new base value.
228         */
229        public void setBase(double base) {
230            this.base = base;
231            notifyListeners(new RendererChangeEvent(this));
232        }
233        
234        /**
235         * Returns a flag that determines whether the y-interval from the dataset is
236         * used to calculate the length of each bar.
237         * 
238         * @return A boolean.
239         */
240        public boolean getUseYInterval() {
241            return this.useYInterval;
242        }
243        
244        /**
245         * Sets the flag that determines whether the y-interval from the dataset is
246         * used to calculate the length of each bar, and sends a 
247         * {@link RendererChangeEvent} to all registered listeners..
248         * 
249         * @param use  the flag.
250         */
251        public void setUseYInterval(boolean use) {
252            this.useYInterval = use;
253            notifyListeners(new RendererChangeEvent(this));
254        }
255    
256        /**
257         * Returns the margin which is a percentage amount by which the bars are 
258         * trimmed.
259         *
260         * @return The margin.
261         */
262        public double getMargin() {
263            return this.margin;
264        }
265        
266        /**
267         * Sets the percentage amount by which the bars are trimmed and sends a 
268         * {@link RendererChangeEvent} to all registered listeners.
269         *
270         * @param margin  the new margin.
271         */
272        public void setMargin(double margin) {
273            this.margin = margin;
274            notifyListeners(new RendererChangeEvent(this));
275        }
276    
277        /**
278         * Returns a flag that controls whether or not bar outlines are drawn.
279         * 
280         * @return A boolean.
281         */
282        public boolean isDrawBarOutline() {
283            return this.drawBarOutline;    
284        }
285        
286        /**
287         * Sets the flag that controls whether or not bar outlines are drawn and 
288         * sends a {@link RendererChangeEvent} to all registered listeners.
289         * 
290         * @param draw  the flag.
291         */
292        public void setDrawBarOutline(boolean draw) {
293            this.drawBarOutline = draw;
294            notifyListeners(new RendererChangeEvent(this));
295        }
296        
297        /**
298         * Returns the gradient paint transformer (an object used to transform 
299         * gradient paint objects to fit each bar.
300         * 
301         * @return A transformer (<code>null</code> possible).
302         */    
303        public GradientPaintTransformer getGradientPaintTransformer() {
304            return this.gradientPaintTransformer;    
305        }
306        
307        /**
308         * Sets the gradient paint transformer and sends a 
309         * {@link RendererChangeEvent} to all registered listeners.
310         * 
311         * @param transformer  the transformer (<code>null</code> permitted).
312         */
313        public void setGradientPaintTransformer(
314                GradientPaintTransformer transformer) {
315            this.gradientPaintTransformer = transformer;
316            notifyListeners(new RendererChangeEvent(this));
317        }
318         
319        /**
320         * Returns the shape used to represent bars in each legend item.
321         * 
322         * @return The shape used to represent bars in each legend item (never 
323         *         <code>null</code>).
324         */
325        public Shape getLegendBar() {
326            return this.legendBar;
327        }
328        
329        /**
330         * Sets the shape used to represent bars in each legend item.
331         * 
332         * @param bar  the bar shape (<code>null</code> not permitted).
333         */
334        public void setLegendBar(Shape bar) {
335            if (bar == null) {
336                throw new IllegalArgumentException("Null 'bar' argument.");
337            }
338            this.legendBar = bar;
339            notifyListeners(new RendererChangeEvent(this));
340        }
341        
342        /**
343         * Initialises the renderer and returns a state object that should be 
344         * passed to all subsequent calls to the drawItem() method.  Here we 
345         * calculate the Java2D y-coordinate for zero, since all the bars have 
346         * their bases fixed at zero.
347         *
348         * @param g2  the graphics device.
349         * @param dataArea  the area inside the axes.
350         * @param plot  the plot.
351         * @param dataset  the data.
352         * @param info  an optional info collection object to return data back to 
353         *              the caller.
354         *
355         * @return A state object.
356         */
357        public XYItemRendererState initialise(Graphics2D g2,
358                                              Rectangle2D dataArea,
359                                              XYPlot plot,
360                                              XYDataset dataset,
361                                              PlotRenderingInfo info) {
362    
363            XYBarRendererState state = new XYBarRendererState(info);
364            ValueAxis rangeAxis 
365                = plot.getRangeAxisForDataset(plot.indexOf(dataset));
366            state.setG2Base(
367                rangeAxis.valueToJava2D(
368                    this.base, dataArea, plot.getRangeAxisEdge()
369                )
370            );
371            return state;
372    
373        }
374    
375        /**
376         * Returns a default legend item for the specified series.  Subclasses 
377         * should override this method to generate customised items.
378         *
379         * @param datasetIndex  the dataset index (zero-based).
380         * @param series  the series index (zero-based).
381         *
382         * @return A legend item for the series.
383         */
384        public LegendItem getLegendItem(int datasetIndex, int series) {
385            LegendItem result = null;
386            XYPlot xyplot = getPlot();
387            if (xyplot != null) {
388                XYDataset dataset = xyplot.getDataset(datasetIndex);
389                if (dataset != null) {
390                    XYSeriesLabelGenerator lg = getLegendItemLabelGenerator();
391                    String label = lg.generateLabel(dataset, series);
392                    String description = label;
393                    String toolTipText = null;
394                    if (getLegendItemToolTipGenerator() != null) {
395                        toolTipText = getLegendItemToolTipGenerator().generateLabel(
396                            dataset, series
397                        );
398                    }
399                    String urlText = null;
400                    if (getLegendItemURLGenerator() != null) {
401                        urlText = getLegendItemURLGenerator().generateLabel(
402                            dataset, series
403                        );
404                    }
405                    Shape shape = this.legendBar;
406                    Paint paint = getSeriesPaint(series);
407                    Paint outlinePaint = getSeriesOutlinePaint(series);
408                    Stroke outlineStroke = getSeriesOutlineStroke(series);
409                    result = new LegendItem(label, description, toolTipText, 
410                            urlText, shape, paint, outlineStroke, outlinePaint);
411                }
412            }
413            return result;
414        }
415        
416        /**
417         * Draws the visual representation of a single data item.
418         *
419         * @param g2  the graphics device.
420         * @param state  the renderer state.
421         * @param dataArea  the area within which the plot is being drawn.
422         * @param info  collects information about the drawing.
423         * @param plot  the plot (can be used to obtain standard color 
424         *              information etc).
425         * @param domainAxis  the domain axis.
426         * @param rangeAxis  the range axis.
427         * @param dataset  the dataset.
428         * @param series  the series index (zero-based).
429         * @param item  the item index (zero-based).
430         * @param crosshairState  crosshair information for the plot 
431         *                        (<code>null</code> permitted).
432         * @param pass  the pass index.
433         */
434        public void drawItem(Graphics2D g2,
435                             XYItemRendererState state,
436                             Rectangle2D dataArea,
437                             PlotRenderingInfo info,
438                             XYPlot plot,
439                             ValueAxis domainAxis,
440                             ValueAxis rangeAxis,
441                             XYDataset dataset,
442                             int series,
443                             int item,
444                             CrosshairState crosshairState,
445                             int pass) {
446    
447            if (!getItemVisible(series, item)) {
448                return;   
449            }
450            IntervalXYDataset intervalDataset = (IntervalXYDataset) dataset;
451    
452            double value0;
453            double value1;
454            if (this.useYInterval) {
455                value0 = intervalDataset.getStartYValue(series, item);
456                value1 = intervalDataset.getEndYValue(series, item);
457            }
458            else {
459                value0 = this.base;
460                value1 = intervalDataset.getYValue(series, item);
461            }
462            if (Double.isNaN(value0) || Double.isNaN(value1)) {
463                return;
464            }
465    
466            double translatedValue0 = rangeAxis.valueToJava2D(
467                value0, dataArea, plot.getRangeAxisEdge()
468            );
469            double translatedValue1 = rangeAxis.valueToJava2D(
470                value1, dataArea, plot.getRangeAxisEdge()
471            );
472    
473            RectangleEdge location = plot.getDomainAxisEdge();
474            Number startXNumber = intervalDataset.getStartX(series, item);
475            if (startXNumber == null) {
476                return;
477            }
478            double translatedStartX = domainAxis.valueToJava2D(
479                startXNumber.doubleValue(), dataArea, location
480            );
481    
482            Number endXNumber = intervalDataset.getEndX(series, item);
483            if (endXNumber == null) {
484                return;
485            }
486            double translatedEndX = domainAxis.valueToJava2D(
487                endXNumber.doubleValue(), dataArea, location
488            );
489    
490            double translatedWidth = Math.max(
491                1, Math.abs(translatedEndX - translatedStartX)
492            );
493            double translatedHeight = Math.abs(translatedValue1 - translatedValue0);
494    
495            if (getMargin() > 0.0) {
496                double cut = translatedWidth * getMargin();
497                translatedWidth = translatedWidth - cut;
498                translatedStartX = translatedStartX + cut / 2;
499            }
500    
501            Rectangle2D bar = null;
502            PlotOrientation orientation = plot.getOrientation();
503            if (orientation == PlotOrientation.HORIZONTAL) {
504                bar = new Rectangle2D.Double(
505                    Math.min(translatedValue0, translatedValue1), 
506                    Math.min(translatedStartX, translatedEndX),
507                    translatedHeight, translatedWidth);
508            }
509            else if (orientation == PlotOrientation.VERTICAL) {
510                bar = new Rectangle2D.Double(
511                    Math.min(translatedStartX, translatedEndX), 
512                    Math.min(translatedValue0, translatedValue1), 
513                    translatedWidth, translatedHeight);
514            }
515    
516            Paint itemPaint = getItemPaint(series, item);
517            if (getGradientPaintTransformer() 
518                    != null && itemPaint instanceof GradientPaint) {
519                GradientPaint gp = (GradientPaint) itemPaint;
520                itemPaint = getGradientPaintTransformer().transform(gp, bar);
521            }
522            g2.setPaint(itemPaint);
523            g2.fill(bar);
524            if (isDrawBarOutline() 
525                    && Math.abs(translatedEndX - translatedStartX) > 3) {
526                Stroke stroke = getItemOutlineStroke(series, item);
527                Paint paint = getItemOutlinePaint(series, item);
528                if (stroke != null && paint != null) {
529                    g2.setStroke(stroke);
530                    g2.setPaint(paint);
531                    g2.draw(bar);                
532                }
533            }
534            
535            // TODO: we need something better for the item labels
536            if (isItemLabelVisible(series, item)) {
537                drawItemLabel(
538                    g2, orientation, dataset, series, item, bar.getCenterX(), 
539                    bar.getY(), value1 < 0.0
540                );
541            }
542    
543            // add an entity for the item...
544            if (info != null) {
545                EntityCollection entities = info.getOwner().getEntityCollection();
546                if (entities != null) {
547                    String tip = null;
548                    XYToolTipGenerator generator 
549                        = getToolTipGenerator(series, item);
550                    if (generator != null) {
551                        tip = generator.generateToolTip(dataset, series, item);
552                    }
553                    String url = null;
554                    if (getURLGenerator() != null) {
555                        url = getURLGenerator().generateURL(dataset, series, item);
556                    }
557                    XYItemEntity entity = new XYItemEntity(
558                        bar, dataset, series, item, tip, url
559                    );
560                    entities.add(entity);
561                }
562            }
563    
564        }
565    
566        /**
567         * Returns the lower and upper bounds (range) of the x-values in the 
568         * specified dataset.  Since this renderer uses the x-interval in the 
569         * dataset, this is taken into account for the range.
570         * 
571         * @param dataset  the dataset (<code>null</code> permitted).
572         * 
573         * @return The range (<code>null</code> if the dataset is 
574         *         <code>null</code> or empty).
575         */
576        public Range findDomainBounds(XYDataset dataset) {
577            if (dataset != null) {
578                return DatasetUtilities.findDomainBounds(dataset, true);
579            }
580            else {
581                return null;
582            }
583        }
584    
585        /**
586         * Returns a clone of the renderer.
587         *
588         * @return A clone.
589         *
590         * @throws CloneNotSupportedException  if the renderer cannot be cloned.
591         */
592        public Object clone() throws CloneNotSupportedException {
593            XYBarRenderer result = (XYBarRenderer) super.clone();
594            if (this.gradientPaintTransformer != null) {
595                result.gradientPaintTransformer = (GradientPaintTransformer)
596                    ObjectUtilities.clone(this.gradientPaintTransformer);
597            }
598            return result;
599        }
600    
601        /**
602         * Tests this renderer for equality with an arbitrary object.
603         * 
604         * @param obj  the object to test against (<code>null</code> permitted).
605         * 
606         * @return A boolean.
607         */
608        public boolean equals(Object obj) {
609            if (obj == this) {
610                return true;
611            }
612            if (!(obj instanceof XYBarRenderer)) {
613                return false;
614            }
615            if (!super.equals(obj)) {
616                return false;
617            }
618            XYBarRenderer that = (XYBarRenderer) obj;
619            if (this.base != that.base) {
620                return false;
621            }
622            if (this.drawBarOutline != that.drawBarOutline) {
623                return false;
624            }
625            if (this.margin != that.margin) {
626                return false;
627            }
628            if (this.useYInterval != that.useYInterval) {
629                return false;
630            }
631            if (!ObjectUtilities.equal(
632                this.gradientPaintTransformer, that.gradientPaintTransformer)
633            ) {
634                return false;
635            }
636            if (!ShapeUtilities.equal(this.legendBar, that.legendBar)) {
637                return false;   
638            }
639            return true;
640        }
641        
642        /**
643         * Provides serialization support.
644         *
645         * @param stream  the input stream.
646         *
647         * @throws IOException  if there is an I/O error.
648         * @throws ClassNotFoundException  if there is a classpath problem.
649         */
650        private void readObject(ObjectInputStream stream) 
651                throws IOException, ClassNotFoundException {
652            stream.defaultReadObject();
653            this.legendBar = SerialUtilities.readShape(stream);
654        }
655        
656        /**
657         * Provides serialization support.
658         *
659         * @param stream  the output stream.
660         *
661         * @throws IOException  if there is an I/O error.
662         */
663        private void writeObject(ObjectOutputStream stream) throws IOException {
664            stream.defaultWriteObject();
665            SerialUtilities.writeShape(this.legendBar, stream);
666        }
667    
668    }