001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2006, 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     * LevelRenderer.java
029     * ------------------
030     * (C) Copyright 2004, 2006, by Object Refinery Limited.
031     *
032     * Original Author:  David Gilbert (for Object Refinery Limited);
033     * Contributor(s):   -;
034     *
035     * $Id: LevelRenderer.java,v 1.7.2.3 2006/01/23 09:53:58 mungady Exp $
036     *
037     * Changes
038     * -------
039     * 09-Jan-2004 : Version 1 (DG);
040     * 05-Nov-2004 : Modified drawItem() signature (DG);
041     * 20-Apr-2005 : Renamed CategoryLabelGenerator 
042     *               --> CategoryItemLabelGenerator (DG);
043     * ------------- JFREECHART 1.0.0 ---------------------------------------------
044     * 23-Jan-2006 : Renamed getMaxItemWidth() --> getMaximumItemWidth() (DG);
045     * 
046     */
047    
048    package org.jfree.chart.renderer.category;
049    
050    import java.awt.Graphics2D;
051    import java.awt.Paint;
052    import java.awt.Stroke;
053    import java.awt.geom.Line2D;
054    import java.awt.geom.Rectangle2D;
055    import java.io.Serializable;
056    
057    import org.jfree.chart.axis.CategoryAxis;
058    import org.jfree.chart.axis.ValueAxis;
059    import org.jfree.chart.entity.CategoryItemEntity;
060    import org.jfree.chart.entity.EntityCollection;
061    import org.jfree.chart.event.RendererChangeEvent;
062    import org.jfree.chart.labels.CategoryItemLabelGenerator;
063    import org.jfree.chart.labels.CategoryToolTipGenerator;
064    import org.jfree.chart.plot.CategoryPlot;
065    import org.jfree.chart.plot.PlotOrientation;
066    import org.jfree.chart.plot.PlotRenderingInfo;
067    import org.jfree.data.category.CategoryDataset;
068    import org.jfree.ui.RectangleEdge;
069    import org.jfree.util.PublicCloneable;
070    
071    /**
072     * A {@link CategoryItemRenderer} that draws individual data items as 
073     * horizontal lines, spaced in the same way as bars in a bar chart.
074     */
075    public class LevelRenderer extends AbstractCategoryItemRenderer 
076                               implements Cloneable, PublicCloneable, Serializable {
077    
078        /** For serialization. */
079        private static final long serialVersionUID = -8204856624355025117L;
080        
081        /** The default item margin percentage. */
082        public static final double DEFAULT_ITEM_MARGIN = 0.20;
083    
084        /** The margin between items within a category. */
085        private double itemMargin;
086    
087        /** The maximum item width as a percentage of the available space. */
088        private double maxItemWidth;
089        
090        /**
091         * Creates a new renderer with default settings.
092         */
093        public LevelRenderer() {
094            super();
095            this.itemMargin = DEFAULT_ITEM_MARGIN;
096            this.maxItemWidth = 1.0;  // 100 percent, so it will not apply unless 
097                                      // changed
098        }
099    
100        /**
101         * Returns the item margin.
102         *
103         * @return The margin.
104         */
105        public double getItemMargin() {
106            return this.itemMargin;
107        }
108    
109        /**
110         * Sets the item margin.  The value is expressed as a percentage of the 
111         * available width for plotting all the bars, with the resulting amount to 
112         * be distributed between all the bars evenly.
113         *
114         * @param percent  the new margin.
115         */
116        public void setItemMargin(double percent) {
117            this.itemMargin = percent;
118            notifyListeners(new RendererChangeEvent(this));
119        }
120        
121        /**
122         * Returns the maximum width, as a percentage of the available drawing 
123         * space.
124         * 
125         * @return The maximum width.
126         * 
127         * @deprecated Use {@link #getMaximumItemWidth()} instead.
128         */
129        public double getMaxItemWidth() {
130            return this.maxItemWidth;
131        }
132        
133        /**
134         * Sets the maximum item width, which is specified as a percentage of the 
135         * available space for all items, and sends a {@link RendererChangeEvent} 
136         * to all registered listeners.
137         * 
138         * @param percent  the percent.
139         * 
140         * @deprecated Use {@link #setMaximumItemWidth(double)} instead.
141         */
142        public void setMaxItemWidth(double percent) {
143            this.maxItemWidth = percent;
144            notifyListeners(new RendererChangeEvent(this));
145        }
146    
147        /**
148         * Returns the maximum width, as a percentage of the available drawing 
149         * space.
150         * 
151         * @return The maximum width.
152         */
153        public double getMaximumItemWidth() {
154            return getMaxItemWidth();
155        }
156        
157        /**
158         * Sets the maximum item width, which is specified as a percentage of the 
159         * available space for all items, and sends a {@link RendererChangeEvent} 
160         * to all registered listeners.
161         * 
162         * @param percent  the percent.
163         */
164        public void setMaximumItemWidth(double percent) {
165            setMaxItemWidth(percent);
166        }
167    
168        /**
169         * Initialises the renderer and returns a state object that will be passed 
170         * to subsequent calls to the drawItem method.
171         * <p>
172         * This method gets called once at the start of the process of drawing a 
173         * chart.
174         *
175         * @param g2  the graphics device.
176         * @param dataArea  the area in which the data is to be plotted.
177         * @param plot  the plot.
178         * @param rendererIndex  the renderer index.
179         * @param info  collects chart rendering information for return to caller.
180         * 
181         * @return The renderer state.
182         *
183         */
184        public CategoryItemRendererState initialise(Graphics2D g2,
185                                                    Rectangle2D dataArea,
186                                                    CategoryPlot plot,
187                                                    int rendererIndex,
188                                                    PlotRenderingInfo info) {
189    
190            CategoryItemRendererState state = super.initialise(g2, dataArea, plot, 
191                    rendererIndex, info);
192            calculateItemWidth(plot, dataArea, rendererIndex, state);
193            return state;
194            
195        }
196        
197        /**
198         * Calculates the bar width and stores it in the renderer state.
199         * 
200         * @param plot  the plot.
201         * @param dataArea  the data area.
202         * @param rendererIndex  the renderer index.
203         * @param state  the renderer state.
204         */
205        protected void calculateItemWidth(CategoryPlot plot, 
206                                          Rectangle2D dataArea, 
207                                          int rendererIndex,
208                                          CategoryItemRendererState state) {
209                                             
210            CategoryAxis domainAxis = getDomainAxis(plot, rendererIndex);
211            CategoryDataset dataset = plot.getDataset(rendererIndex);
212            if (dataset != null) {
213                int columns = dataset.getColumnCount();
214                int rows = dataset.getRowCount();
215                double space = 0.0;
216                PlotOrientation orientation = plot.getOrientation();
217                if (orientation == PlotOrientation.HORIZONTAL) {
218                    space = dataArea.getHeight();
219                }
220                else if (orientation == PlotOrientation.VERTICAL) {
221                    space = dataArea.getWidth();
222                }
223                double maxWidth = space * getMaxItemWidth();
224                double categoryMargin = 0.0;
225                double currentItemMargin = 0.0;
226                if (columns > 1) {
227                    categoryMargin = domainAxis.getCategoryMargin();
228                }
229                if (rows > 1) {
230                    currentItemMargin = getItemMargin();
231                }
232                double used = space * (1 - domainAxis.getLowerMargin() 
233                                         - domainAxis.getUpperMargin()
234                                         - categoryMargin - currentItemMargin);
235                if ((rows * columns) > 0) {
236                    state.setBarWidth(Math.min(used / (rows * columns), maxWidth));
237                }
238                else {
239                    state.setBarWidth(Math.min(used, maxWidth));
240                }
241            }
242        }
243    
244        /**
245         * Calculates the coordinate of the first "side" of a bar.  This will be 
246         * the minimum x-coordinate for a vertical bar, and the minimum 
247         * y-coordinate for a horizontal bar.
248         * 
249         * @param plot  the plot.
250         * @param orientation  the plot orientation.
251         * @param dataArea  the data area.
252         * @param domainAxis  the domain axis.
253         * @param state  the renderer state (has the bar width precalculated).
254         * @param row  the row index.
255         * @param column  the column index.
256         * 
257         * @return The coordinate.
258         */
259        protected double calculateBarW0(CategoryPlot plot, 
260                                        PlotOrientation orientation, 
261                                        Rectangle2D dataArea,
262                                        CategoryAxis domainAxis,
263                                        CategoryItemRendererState state,
264                                        int row,
265                                        int column) {
266            // calculate bar width...
267            double space = 0.0;
268            if (orientation == PlotOrientation.HORIZONTAL) {
269                space = dataArea.getHeight();
270            }
271            else {
272                space = dataArea.getWidth();
273            }
274            double barW0 = domainAxis.getCategoryStart(column, getColumnCount(), 
275                    dataArea, plot.getDomainAxisEdge());
276            int seriesCount = getRowCount();
277            int categoryCount = getColumnCount();
278            if (seriesCount > 1) {
279                double seriesGap = space * getItemMargin() 
280                        / (categoryCount * (seriesCount - 1));
281                double seriesW = calculateSeriesWidth(space, domainAxis, 
282                        categoryCount, seriesCount);
283                barW0 = barW0 + row * (seriesW + seriesGap) 
284                              + (seriesW / 2.0) - (state.getBarWidth() / 2.0);
285            }
286            else {
287                barW0 = domainAxis.getCategoryMiddle(column, getColumnCount(), 
288                        dataArea, plot.getDomainAxisEdge()) - state.getBarWidth() 
289                        / 2.0;
290            }
291            return barW0;
292        }
293        
294        /**
295         * Draws the bar for a single (series, category) data item.
296         *
297         * @param g2  the graphics device.
298         * @param state  the renderer state.
299         * @param dataArea  the data area.
300         * @param plot  the plot.
301         * @param domainAxis  the domain axis.
302         * @param rangeAxis  the range axis.
303         * @param dataset  the dataset.
304         * @param row  the row index (zero-based).
305         * @param column  the column index (zero-based).
306         * @param pass  the pass index.
307         */
308        public void drawItem(Graphics2D g2, CategoryItemRendererState state,
309                Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis,
310                ValueAxis rangeAxis, CategoryDataset dataset, int row, int column,
311                int pass) {
312    
313            // nothing is drawn for null values...
314            Number dataValue = dataset.getValue(row, column);
315            if (dataValue == null) {
316                return;
317            }
318            
319            double value = dataValue.doubleValue();
320            
321            PlotOrientation orientation = plot.getOrientation();
322            double barW0 = calculateBarW0(plot, orientation, dataArea, domainAxis, 
323                    state, row, column);
324            RectangleEdge edge = plot.getRangeAxisEdge();
325            double barL = rangeAxis.valueToJava2D(value, dataArea, edge);
326    
327            // draw the bar...
328            Line2D line = null;
329            double x = 0.0;
330            double y = 0.0;
331            if (orientation == PlotOrientation.HORIZONTAL) {
332                x = barL;
333                y = barW0 + state.getBarWidth() / 2.0;
334                line = new Line2D.Double(barL, barW0, barL, 
335                        barW0 + state.getBarWidth());
336            }
337            else {
338                x = barW0 + state.getBarWidth() / 2.0;
339                y = barL;
340                line = new Line2D.Double(barW0, barL, barW0 + state.getBarWidth(), 
341                        barL);
342            }
343            Stroke itemStroke = getItemStroke(row, column);
344            Paint itemPaint = getItemPaint(row, column);
345            g2.setStroke(itemStroke);
346            g2.setPaint(itemPaint);
347            g2.draw(line);
348    
349            CategoryItemLabelGenerator generator = getItemLabelGenerator(row, 
350                    column);
351            if (generator != null && isItemLabelVisible(row, column)) {
352                drawItemLabel(g2, orientation, dataset, row, column, x, y, 
353                        (value < 0.0));
354            }        
355                    
356            // collect entity and tool tip information...
357            if (state.getInfo() != null) {
358                EntityCollection entities = state.getEntityCollection();
359                if (entities != null) {
360                    String tip = null;
361                    CategoryToolTipGenerator tipster = getToolTipGenerator(row, 
362                            column);
363                    if (tipster != null) {
364                        tip = tipster.generateToolTip(dataset, row, column);
365                    }
366                    String url = null;
367                    if (getItemURLGenerator(row, column) != null) {
368                        url = getItemURLGenerator(row, column).generateURL(dataset,
369                                row, column);
370                    }
371                    CategoryItemEntity entity = new CategoryItemEntity(
372                            line.getBounds(), tip, url, dataset, row, 
373                            dataset.getColumnKey(column), column);
374                    entities.add(entity);
375                }
376    
377            }
378    
379        }
380    
381        /**
382         * Calculates the available space for each series.
383         * 
384         * @param space  the space along the entire axis (in Java2D units).
385         * @param axis  the category axis.
386         * @param categories  the number of categories.
387         * @param series  the number of series.
388         * 
389         * @return The width of one series.
390         */
391        protected double calculateSeriesWidth(double space, CategoryAxis axis, 
392                                              int categories, int series) {
393            double factor = 1.0 - getItemMargin() - axis.getLowerMargin() 
394                            - axis.getUpperMargin();
395            if (categories > 1) {
396                factor = factor - axis.getCategoryMargin();
397            }
398            return (space * factor) / (categories * series);
399        }
400        
401        /**
402         * Tests an object for equality with this instance.
403         * 
404         * @param obj  the object (<code>null</code> permitted).
405         * 
406         * @return A boolean.
407         */
408        public boolean equals(Object obj) {
409            if (obj == this) {
410                return true;
411            }
412            if (!(obj instanceof LevelRenderer)) {
413                return false;
414            }
415            if (!super.equals(obj)) {
416                return false;
417            }
418            LevelRenderer that = (LevelRenderer) obj;
419            if (this.itemMargin != that.itemMargin) {              
420                return false;
421            }
422            if (this.maxItemWidth != that.maxItemWidth) {
423                return false;
424            }
425            return true;
426        }
427    
428    }