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