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. The example 072 * shown here is generated by the <code>StackedBarChartDemo4.java</code> 073 * program included in the JFreeChart Demo Collection: 074 * <br><br> 075 * <img src="../../../../../images/GroupedStackedBarRendererSample.png" 076 * alt="GroupedStackedBarRendererSample.png" /> 077 */ 078 public class GroupedStackedBarRenderer extends StackedBarRenderer 079 implements Cloneable, PublicCloneable, 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, and sends a 097 * {@link RendererChangeEvent} to all registered listeners. 098 * 099 * @param map the map (<code>null</code> not permitted). 100 */ 101 public void setSeriesToGroupMap(KeyToGroupMap map) { 102 if (map == null) { 103 throw new IllegalArgumentException("Null 'map' argument."); 104 } 105 this.seriesToGroupMap = map; 106 fireChangeEvent(); 107 } 108 109 /** 110 * Returns the range of values the renderer requires to display all the 111 * items from the specified dataset. 112 * 113 * @param dataset the dataset (<code>null</code> permitted). 114 * 115 * @return The range (or <code>null</code> if the dataset is 116 * <code>null</code> or empty). 117 */ 118 public Range findRangeBounds(CategoryDataset dataset) { 119 if (dataset == null) { 120 return null; 121 } 122 Range r = DatasetUtilities.findStackedRangeBounds( 123 dataset, this.seriesToGroupMap); 124 return r; 125 } 126 127 /** 128 * Calculates the bar width and stores it in the renderer state. We 129 * override the method in the base class to take account of the 130 * series-to-group mapping. 131 * 132 * @param plot the plot. 133 * @param dataArea the data area. 134 * @param rendererIndex the renderer index. 135 * @param state the renderer state. 136 */ 137 protected void calculateBarWidth(CategoryPlot plot, 138 Rectangle2D dataArea, 139 int rendererIndex, 140 CategoryItemRendererState state) { 141 142 // calculate the bar width 143 CategoryAxis xAxis = plot.getDomainAxisForDataset(rendererIndex); 144 CategoryDataset data = plot.getDataset(rendererIndex); 145 if (data != null) { 146 PlotOrientation orientation = plot.getOrientation(); 147 double space = 0.0; 148 if (orientation == PlotOrientation.HORIZONTAL) { 149 space = dataArea.getHeight(); 150 } 151 else if (orientation == PlotOrientation.VERTICAL) { 152 space = dataArea.getWidth(); 153 } 154 double maxWidth = space * getMaximumBarWidth(); 155 int groups = this.seriesToGroupMap.getGroupCount(); 156 int categories = data.getColumnCount(); 157 int columns = groups * categories; 158 double categoryMargin = 0.0; 159 double itemMargin = 0.0; 160 if (categories > 1) { 161 categoryMargin = xAxis.getCategoryMargin(); 162 } 163 if (groups > 1) { 164 itemMargin = getItemMargin(); 165 } 166 167 double used = space * (1 - xAxis.getLowerMargin() 168 - xAxis.getUpperMargin() 169 - categoryMargin - itemMargin); 170 if (columns > 0) { 171 state.setBarWidth(Math.min(used / columns, maxWidth)); 172 } 173 else { 174 state.setBarWidth(Math.min(used, maxWidth)); 175 } 176 } 177 178 } 179 180 /** 181 * Calculates the coordinate of the first "side" of a bar. This will be 182 * the minimum x-coordinate for a vertical bar, and the minimum 183 * y-coordinate for a horizontal bar. 184 * 185 * @param plot the plot. 186 * @param orientation the plot orientation. 187 * @param dataArea the data area. 188 * @param domainAxis the domain axis. 189 * @param state the renderer state (has the bar width precalculated). 190 * @param row the row index. 191 * @param column the column index. 192 * 193 * @return The coordinate. 194 */ 195 protected double calculateBarW0(CategoryPlot plot, 196 PlotOrientation orientation, 197 Rectangle2D dataArea, 198 CategoryAxis domainAxis, 199 CategoryItemRendererState state, 200 int row, 201 int column) { 202 // calculate bar width... 203 double space = 0.0; 204 if (orientation == PlotOrientation.HORIZONTAL) { 205 space = dataArea.getHeight(); 206 } 207 else { 208 space = dataArea.getWidth(); 209 } 210 double barW0 = domainAxis.getCategoryStart(column, getColumnCount(), 211 dataArea, plot.getDomainAxisEdge()); 212 int groupCount = this.seriesToGroupMap.getGroupCount(); 213 int groupIndex = this.seriesToGroupMap.getGroupIndex( 214 this.seriesToGroupMap.getGroup(plot.getDataset( 215 plot.getIndexOf(this)).getRowKey(row))); 216 int categoryCount = getColumnCount(); 217 if (groupCount > 1) { 218 double groupGap = space * getItemMargin() 219 / (categoryCount * (groupCount - 1)); 220 double groupW = calculateSeriesWidth(space, domainAxis, 221 categoryCount, groupCount); 222 barW0 = barW0 + groupIndex * (groupW + groupGap) 223 + (groupW / 2.0) - (state.getBarWidth() / 2.0); 224 } 225 else { 226 barW0 = domainAxis.getCategoryMiddle(column, getColumnCount(), 227 dataArea, plot.getDomainAxisEdge()) 228 - state.getBarWidth() / 2.0; 229 } 230 return barW0; 231 } 232 233 /** 234 * Draws a stacked bar for a specific item. 235 * 236 * @param g2 the graphics device. 237 * @param state the renderer state. 238 * @param dataArea the plot area. 239 * @param plot the plot. 240 * @param domainAxis the domain (category) axis. 241 * @param rangeAxis the range (value) axis. 242 * @param dataset the data. 243 * @param row the row index (zero-based). 244 * @param column the column index (zero-based). 245 * @param pass the pass index. 246 */ 247 public void drawItem(Graphics2D g2, 248 CategoryItemRendererState state, 249 Rectangle2D dataArea, 250 CategoryPlot plot, 251 CategoryAxis domainAxis, 252 ValueAxis rangeAxis, 253 CategoryDataset dataset, 254 int row, 255 int column, 256 int pass) { 257 258 // nothing is drawn for null values... 259 Number dataValue = dataset.getValue(row, column); 260 if (dataValue == null) { 261 return; 262 } 263 264 double value = dataValue.doubleValue(); 265 Comparable group = this.seriesToGroupMap.getGroup( 266 dataset.getRowKey(row)); 267 PlotOrientation orientation = plot.getOrientation(); 268 double barW0 = calculateBarW0(plot, orientation, dataArea, domainAxis, 269 state, row, column); 270 271 double positiveBase = 0.0; 272 double negativeBase = 0.0; 273 274 for (int i = 0; i < row; i++) { 275 if (group.equals(this.seriesToGroupMap.getGroup( 276 dataset.getRowKey(i)))) { 277 Number v = dataset.getValue(i, column); 278 if (v != null) { 279 double d = v.doubleValue(); 280 if (d > 0) { 281 positiveBase = positiveBase + d; 282 } 283 else { 284 negativeBase = negativeBase + d; 285 } 286 } 287 } 288 } 289 290 double translatedBase; 291 double translatedValue; 292 boolean positive = (value > 0.0); 293 boolean inverted = rangeAxis.isInverted(); 294 RectangleEdge barBase; 295 if (orientation == PlotOrientation.HORIZONTAL) { 296 if (positive && inverted || !positive && !inverted) { 297 barBase = RectangleEdge.RIGHT; 298 } 299 else { 300 barBase = RectangleEdge.LEFT; 301 } 302 } 303 else { 304 if (positive && !inverted || !positive && inverted) { 305 barBase = RectangleEdge.BOTTOM; 306 } 307 else { 308 barBase = RectangleEdge.TOP; 309 } 310 } 311 RectangleEdge location = plot.getRangeAxisEdge(); 312 if (value > 0.0) { 313 translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea, 314 location); 315 translatedValue = rangeAxis.valueToJava2D(positiveBase + value, 316 dataArea, location); 317 } 318 else { 319 translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea, 320 location); 321 translatedValue = rangeAxis.valueToJava2D(negativeBase + value, 322 dataArea, location); 323 } 324 double barL0 = Math.min(translatedBase, translatedValue); 325 double barLength = Math.max(Math.abs(translatedValue - translatedBase), 326 getMinimumBarLength()); 327 328 Rectangle2D bar = null; 329 if (orientation == PlotOrientation.HORIZONTAL) { 330 bar = new Rectangle2D.Double(barL0, barW0, barLength, 331 state.getBarWidth()); 332 } 333 else { 334 bar = new Rectangle2D.Double(barW0, barL0, state.getBarWidth(), 335 barLength); 336 } 337 getBarPainter().paintBar(g2, this, row, column, bar, barBase); 338 339 CategoryItemLabelGenerator generator = getItemLabelGenerator(row, 340 column); 341 if (generator != null && isItemLabelVisible(row, column)) { 342 drawItemLabel(g2, dataset, row, column, plot, generator, bar, 343 (value < 0.0)); 344 } 345 346 // collect entity and tool tip information... 347 if (state.getInfo() != null) { 348 EntityCollection entities = state.getEntityCollection(); 349 if (entities != null) { 350 addItemEntity(entities, dataset, row, column, bar); 351 } 352 } 353 354 } 355 356 /** 357 * Tests this renderer for equality with an arbitrary object. 358 * 359 * @param obj the object (<code>null</code> permitted). 360 * 361 * @return A boolean. 362 */ 363 public boolean equals(Object obj) { 364 if (obj == this) { 365 return true; 366 } 367 if (!(obj instanceof GroupedStackedBarRenderer)) { 368 return false; 369 } 370 GroupedStackedBarRenderer that = (GroupedStackedBarRenderer) obj; 371 if (!this.seriesToGroupMap.equals(that.seriesToGroupMap)) { 372 return false; 373 } 374 return super.equals(obj); 375 } 376 377 }