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