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