001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2008, 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 * GroupedStackedBarRenderer.java 029 * ------------------------------ 030 * (C) Copyright 2004-2008, by Object Refinery Limited and Contributors. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): -; 034 * 035 * Changes 036 * ------- 037 * 29-Apr-2004 : Version 1 (DG); 038 * 08-Jul-2004 : Added equals() method (DG); 039 * 05-Nov-2004 : Modified drawItem() signature (DG); 040 * 07-Jan-2005 : Renamed getRangeExtent() --> findRangeBounds (DG); 041 * 20-Apr-2005 : Renamed CategoryLabelGenerator 042 * --> CategoryItemLabelGenerator (DG); 043 * 22-Sep-2005 : Renamed getMaxBarWidth() --> getMaximumBarWidth() (DG); 044 * 20-Dec-2007 : Fix for bug 1848961 (DG); 045 * 24-Jun-2008 : Added new barPainter mechanism (DG); 046 * 047 */ 048 049 package org.jfree.chart.renderer.category; 050 051 import java.awt.Graphics2D; 052 import java.awt.geom.Rectangle2D; 053 import java.io.Serializable; 054 055 import org.jfree.chart.axis.CategoryAxis; 056 import org.jfree.chart.axis.ValueAxis; 057 import org.jfree.chart.entity.EntityCollection; 058 import org.jfree.chart.event.RendererChangeEvent; 059 import org.jfree.chart.labels.CategoryItemLabelGenerator; 060 import org.jfree.chart.plot.CategoryPlot; 061 import org.jfree.chart.plot.PlotOrientation; 062 import org.jfree.data.KeyToGroupMap; 063 import org.jfree.data.Range; 064 import org.jfree.data.category.CategoryDataset; 065 import org.jfree.data.general.DatasetUtilities; 066 import org.jfree.ui.RectangleEdge; 067 import org.jfree.util.PublicCloneable; 068 069 /** 070 * A renderer that draws stacked bars within groups. This will probably be 071 * merged with the {@link StackedBarRenderer} class at some point. 072 */ 073 public class GroupedStackedBarRenderer extends StackedBarRenderer 074 implements Cloneable, PublicCloneable, Serializable { 075 076 /** For serialization. */ 077 private static final long serialVersionUID = -2725921399005922939L; 078 079 /** A map used to assign each series to a group. */ 080 private KeyToGroupMap seriesToGroupMap; 081 082 /** 083 * Creates a new renderer. 084 */ 085 public GroupedStackedBarRenderer() { 086 super(); 087 this.seriesToGroupMap = new KeyToGroupMap(); 088 } 089 090 /** 091 * Updates the map used to assign each series to a group, and sends a 092 * {@link RendererChangeEvent} to all registered listeners. 093 * 094 * @param map the map (<code>null</code> not permitted). 095 */ 096 public void setSeriesToGroupMap(KeyToGroupMap map) { 097 if (map == null) { 098 throw new IllegalArgumentException("Null 'map' argument."); 099 } 100 this.seriesToGroupMap = map; 101 fireChangeEvent(); 102 } 103 104 /** 105 * Returns the range of values the renderer requires to display all the 106 * items from the specified dataset. 107 * 108 * @param dataset the dataset (<code>null</code> permitted). 109 * 110 * @return The range (or <code>null</code> if the dataset is 111 * <code>null</code> or empty). 112 */ 113 public Range findRangeBounds(CategoryDataset dataset) { 114 Range r = DatasetUtilities.findStackedRangeBounds( 115 dataset, this.seriesToGroupMap); 116 return r; 117 } 118 119 /** 120 * Calculates the bar width and stores it in the renderer state. We 121 * override the method in the base class to take account of the 122 * series-to-group mapping. 123 * 124 * @param plot the plot. 125 * @param dataArea the data area. 126 * @param rendererIndex the renderer index. 127 * @param state the renderer state. 128 */ 129 protected void calculateBarWidth(CategoryPlot plot, 130 Rectangle2D dataArea, 131 int rendererIndex, 132 CategoryItemRendererState state) { 133 134 // calculate the bar width 135 CategoryAxis xAxis = plot.getDomainAxisForDataset(rendererIndex); 136 CategoryDataset data = plot.getDataset(rendererIndex); 137 if (data != null) { 138 PlotOrientation orientation = plot.getOrientation(); 139 double space = 0.0; 140 if (orientation == PlotOrientation.HORIZONTAL) { 141 space = dataArea.getHeight(); 142 } 143 else if (orientation == PlotOrientation.VERTICAL) { 144 space = dataArea.getWidth(); 145 } 146 double maxWidth = space * getMaximumBarWidth(); 147 int groups = this.seriesToGroupMap.getGroupCount(); 148 int categories = data.getColumnCount(); 149 int columns = groups * categories; 150 double categoryMargin = 0.0; 151 double itemMargin = 0.0; 152 if (categories > 1) { 153 categoryMargin = xAxis.getCategoryMargin(); 154 } 155 if (groups > 1) { 156 itemMargin = getItemMargin(); 157 } 158 159 double used = space * (1 - xAxis.getLowerMargin() 160 - xAxis.getUpperMargin() 161 - categoryMargin - itemMargin); 162 if (columns > 0) { 163 state.setBarWidth(Math.min(used / columns, maxWidth)); 164 } 165 else { 166 state.setBarWidth(Math.min(used, maxWidth)); 167 } 168 } 169 170 } 171 172 /** 173 * Calculates the coordinate of the first "side" of a bar. This will be 174 * the minimum x-coordinate for a vertical bar, and the minimum 175 * y-coordinate for a horizontal bar. 176 * 177 * @param plot the plot. 178 * @param orientation the plot orientation. 179 * @param dataArea the data area. 180 * @param domainAxis the domain axis. 181 * @param state the renderer state (has the bar width precalculated). 182 * @param row the row index. 183 * @param column the column index. 184 * 185 * @return The coordinate. 186 */ 187 protected double calculateBarW0(CategoryPlot plot, 188 PlotOrientation orientation, 189 Rectangle2D dataArea, 190 CategoryAxis domainAxis, 191 CategoryItemRendererState state, 192 int row, 193 int column) { 194 // calculate bar width... 195 double space = 0.0; 196 if (orientation == PlotOrientation.HORIZONTAL) { 197 space = dataArea.getHeight(); 198 } 199 else { 200 space = dataArea.getWidth(); 201 } 202 double barW0 = domainAxis.getCategoryStart(column, getColumnCount(), 203 dataArea, plot.getDomainAxisEdge()); 204 int groupCount = this.seriesToGroupMap.getGroupCount(); 205 int groupIndex = this.seriesToGroupMap.getGroupIndex( 206 this.seriesToGroupMap.getGroup(plot.getDataset( 207 plot.getIndexOf(this)).getRowKey(row))); 208 int categoryCount = getColumnCount(); 209 if (groupCount > 1) { 210 double groupGap = space * getItemMargin() 211 / (categoryCount * (groupCount - 1)); 212 double groupW = calculateSeriesWidth(space, domainAxis, 213 categoryCount, groupCount); 214 barW0 = barW0 + groupIndex * (groupW + groupGap) 215 + (groupW / 2.0) - (state.getBarWidth() / 2.0); 216 } 217 else { 218 barW0 = domainAxis.getCategoryMiddle(column, getColumnCount(), 219 dataArea, plot.getDomainAxisEdge()) 220 - state.getBarWidth() / 2.0; 221 } 222 return barW0; 223 } 224 225 /** 226 * Draws a stacked bar for a specific item. 227 * 228 * @param g2 the graphics device. 229 * @param state the renderer state. 230 * @param dataArea the plot area. 231 * @param plot the plot. 232 * @param domainAxis the domain (category) axis. 233 * @param rangeAxis the range (value) axis. 234 * @param dataset the data. 235 * @param row the row index (zero-based). 236 * @param column the column index (zero-based). 237 * @param pass the pass index. 238 */ 239 public void drawItem(Graphics2D g2, 240 CategoryItemRendererState state, 241 Rectangle2D dataArea, 242 CategoryPlot plot, 243 CategoryAxis domainAxis, 244 ValueAxis rangeAxis, 245 CategoryDataset dataset, 246 int row, 247 int column, 248 int pass) { 249 250 // nothing is drawn for null values... 251 Number dataValue = dataset.getValue(row, column); 252 if (dataValue == null) { 253 return; 254 } 255 256 double value = dataValue.doubleValue(); 257 Comparable group = this.seriesToGroupMap.getGroup( 258 dataset.getRowKey(row)); 259 PlotOrientation orientation = plot.getOrientation(); 260 double barW0 = calculateBarW0(plot, orientation, dataArea, domainAxis, 261 state, row, column); 262 263 double positiveBase = 0.0; 264 double negativeBase = 0.0; 265 266 for (int i = 0; i < row; i++) { 267 if (group.equals(this.seriesToGroupMap.getGroup( 268 dataset.getRowKey(i)))) { 269 Number v = dataset.getValue(i, column); 270 if (v != null) { 271 double d = v.doubleValue(); 272 if (d > 0) { 273 positiveBase = positiveBase + d; 274 } 275 else { 276 negativeBase = negativeBase + d; 277 } 278 } 279 } 280 } 281 282 double translatedBase; 283 double translatedValue; 284 boolean positive = (value > 0.0); 285 boolean inverted = rangeAxis.isInverted(); 286 RectangleEdge barBase; 287 if (orientation == PlotOrientation.HORIZONTAL) { 288 if (positive && inverted || !positive && !inverted) { 289 barBase = RectangleEdge.RIGHT; 290 } 291 else { 292 barBase = RectangleEdge.LEFT; 293 } 294 } 295 else { 296 if (positive && !inverted || !positive && inverted) { 297 barBase = RectangleEdge.BOTTOM; 298 } 299 else { 300 barBase = RectangleEdge.TOP; 301 } 302 } 303 RectangleEdge location = plot.getRangeAxisEdge(); 304 if (value > 0.0) { 305 translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea, 306 location); 307 translatedValue = rangeAxis.valueToJava2D(positiveBase + value, 308 dataArea, location); 309 } 310 else { 311 translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea, 312 location); 313 translatedValue = rangeAxis.valueToJava2D(negativeBase + value, 314 dataArea, location); 315 } 316 double barL0 = Math.min(translatedBase, translatedValue); 317 double barLength = Math.max(Math.abs(translatedValue - translatedBase), 318 getMinimumBarLength()); 319 320 Rectangle2D bar = null; 321 if (orientation == PlotOrientation.HORIZONTAL) { 322 bar = new Rectangle2D.Double(barL0, barW0, barLength, 323 state.getBarWidth()); 324 } 325 else { 326 bar = new Rectangle2D.Double(barW0, barL0, state.getBarWidth(), 327 barLength); 328 } 329 getBarPainter().paintBar(g2, this, row, column, bar, barBase); 330 331 CategoryItemLabelGenerator generator = getItemLabelGenerator(row, 332 column); 333 if (generator != null && isItemLabelVisible(row, column)) { 334 drawItemLabel(g2, dataset, row, column, plot, generator, bar, 335 (value < 0.0)); 336 } 337 338 // collect entity and tool tip information... 339 if (state.getInfo() != null) { 340 EntityCollection entities = state.getEntityCollection(); 341 if (entities != null) { 342 addItemEntity(entities, dataset, row, column, bar); 343 } 344 } 345 346 } 347 348 /** 349 * Tests this renderer for equality with an arbitrary object. 350 * 351 * @param obj the object (<code>null</code> permitted). 352 * 353 * @return A boolean. 354 */ 355 public boolean equals(Object obj) { 356 if (obj == this) { 357 return true; 358 } 359 if (!(obj instanceof GroupedStackedBarRenderer)) { 360 return false; 361 } 362 GroupedStackedBarRenderer that = (GroupedStackedBarRenderer) obj; 363 if (!this.seriesToGroupMap.equals(that.seriesToGroupMap)) { 364 return false; 365 } 366 return super.equals(obj); 367 } 368 369 }