001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2014, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * ------------------------------ 028 * GroupedStackedBarRenderer.java 029 * ------------------------------ 030 * (C) Copyright 2004-2014, 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 * 03-Jul-2013 : Use ParamChecks (DG); 047 * 048 */ 049 050package org.jfree.chart.renderer.category; 051 052import java.awt.Graphics2D; 053import java.awt.geom.Rectangle2D; 054import java.io.Serializable; 055 056import org.jfree.chart.axis.CategoryAxis; 057import org.jfree.chart.axis.ValueAxis; 058import org.jfree.chart.entity.EntityCollection; 059import org.jfree.chart.event.RendererChangeEvent; 060import org.jfree.chart.labels.CategoryItemLabelGenerator; 061import org.jfree.chart.plot.CategoryPlot; 062import org.jfree.chart.plot.PlotOrientation; 063import org.jfree.chart.util.ParamChecks; 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. The example 074 * shown here is generated by the <code>StackedBarChartDemo4.java</code> 075 * program included in the JFreeChart Demo Collection: 076 * <br><br> 077 * <img src="../../../../../images/GroupedStackedBarRendererSample.png" 078 * alt="GroupedStackedBarRendererSample.png"> 079 */ 080public class GroupedStackedBarRenderer extends StackedBarRenderer 081 implements Cloneable, PublicCloneable, Serializable { 082 083 /** For serialization. */ 084 private static final long serialVersionUID = -2725921399005922939L; 085 086 /** A map used to assign each series to a group. */ 087 private KeyToGroupMap seriesToGroupMap; 088 089 /** 090 * Creates a new renderer. 091 */ 092 public GroupedStackedBarRenderer() { 093 super(); 094 this.seriesToGroupMap = new KeyToGroupMap(); 095 } 096 097 /** 098 * Updates the map used to assign each series to a group, and sends a 099 * {@link RendererChangeEvent} to all registered listeners. 100 * 101 * @param map the map (<code>null</code> not permitted). 102 */ 103 public void setSeriesToGroupMap(KeyToGroupMap map) { 104 ParamChecks.nullNotPermitted(map, "map"); 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 @Override 119 public Range findRangeBounds(CategoryDataset dataset) { 120 if (dataset == null) { 121 return null; 122 } 123 Range r = DatasetUtilities.findStackedRangeBounds( 124 dataset, this.seriesToGroupMap); 125 return r; 126 } 127 128 /** 129 * Calculates the bar width and stores it in the renderer state. We 130 * override the method in the base class to take account of the 131 * series-to-group mapping. 132 * 133 * @param plot the plot. 134 * @param dataArea the data area. 135 * @param rendererIndex the renderer index. 136 * @param state the renderer state. 137 */ 138 @Override 139 protected void calculateBarWidth(CategoryPlot plot, Rectangle2D dataArea, 140 int rendererIndex, 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 @Override 196 protected double calculateBarW0(CategoryPlot plot, 197 PlotOrientation orientation, Rectangle2D dataArea, 198 CategoryAxis domainAxis, CategoryItemRendererState state, 199 int row, int column) { 200 // calculate bar width... 201 double space; 202 if (orientation == PlotOrientation.HORIZONTAL) { 203 space = dataArea.getHeight(); 204 } 205 else { 206 space = dataArea.getWidth(); 207 } 208 double barW0 = domainAxis.getCategoryStart(column, getColumnCount(), 209 dataArea, plot.getDomainAxisEdge()); 210 int groupCount = this.seriesToGroupMap.getGroupCount(); 211 int groupIndex = this.seriesToGroupMap.getGroupIndex( 212 this.seriesToGroupMap.getGroup(plot.getDataset( 213 plot.getIndexOf(this)).getRowKey(row))); 214 int categoryCount = getColumnCount(); 215 if (groupCount > 1) { 216 double groupGap = space * getItemMargin() 217 / (categoryCount * (groupCount - 1)); 218 double groupW = calculateSeriesWidth(space, domainAxis, 219 categoryCount, groupCount); 220 barW0 = barW0 + groupIndex * (groupW + groupGap) 221 + (groupW / 2.0) - (state.getBarWidth() / 2.0); 222 } 223 else { 224 barW0 = domainAxis.getCategoryMiddle(column, getColumnCount(), 225 dataArea, plot.getDomainAxisEdge()) 226 - state.getBarWidth() / 2.0; 227 } 228 return barW0; 229 } 230 231 /** 232 * Draws a stacked bar for a specific item. 233 * 234 * @param g2 the graphics device. 235 * @param state the renderer state. 236 * @param dataArea the plot area. 237 * @param plot the plot. 238 * @param domainAxis the domain (category) axis. 239 * @param rangeAxis the range (value) axis. 240 * @param dataset the data. 241 * @param row the row index (zero-based). 242 * @param column the column index (zero-based). 243 * @param pass the pass index. 244 */ 245 @Override 246 public void drawItem(Graphics2D g2, CategoryItemRendererState state, 247 Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 248 ValueAxis rangeAxis, CategoryDataset dataset, int row, 249 int column, 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 boolean positive = (value > 0.0); 286 boolean inverted = rangeAxis.isInverted(); 287 RectangleEdge barBase; 288 if (orientation == PlotOrientation.HORIZONTAL) { 289 if (positive && inverted || !positive && !inverted) { 290 barBase = RectangleEdge.RIGHT; 291 } 292 else { 293 barBase = RectangleEdge.LEFT; 294 } 295 } 296 else { 297 if (positive && !inverted || !positive && inverted) { 298 barBase = RectangleEdge.BOTTOM; 299 } 300 else { 301 barBase = RectangleEdge.TOP; 302 } 303 } 304 RectangleEdge location = plot.getRangeAxisEdge(); 305 if (value > 0.0) { 306 translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea, 307 location); 308 translatedValue = rangeAxis.valueToJava2D(positiveBase + value, 309 dataArea, location); 310 } 311 else { 312 translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea, 313 location); 314 translatedValue = rangeAxis.valueToJava2D(negativeBase + value, 315 dataArea, location); 316 } 317 double barL0 = Math.min(translatedBase, translatedValue); 318 double barLength = Math.max(Math.abs(translatedValue - translatedBase), 319 getMinimumBarLength()); 320 321 Rectangle2D bar; 322 if (orientation == PlotOrientation.HORIZONTAL) { 323 bar = new Rectangle2D.Double(barL0, barW0, barLength, 324 state.getBarWidth()); 325 } 326 else { 327 bar = new Rectangle2D.Double(barW0, barL0, state.getBarWidth(), 328 barLength); 329 } 330 getBarPainter().paintBar(g2, this, row, column, bar, barBase); 331 332 CategoryItemLabelGenerator generator = getItemLabelGenerator(row, 333 column); 334 if (generator != null && isItemLabelVisible(row, column)) { 335 drawItemLabel(g2, dataset, row, column, plot, generator, bar, 336 (value < 0.0)); 337 } 338 339 // collect entity and tool tip information... 340 if (state.getInfo() != null) { 341 EntityCollection entities = state.getEntityCollection(); 342 if (entities != null) { 343 addItemEntity(entities, dataset, row, column, bar); 344 } 345 } 346 347 } 348 349 /** 350 * Tests this renderer for equality with an arbitrary object. 351 * 352 * @param obj the object (<code>null</code> permitted). 353 * 354 * @return A boolean. 355 */ 356 @Override 357 public boolean equals(Object obj) { 358 if (obj == this) { 359 return true; 360 } 361 if (!(obj instanceof GroupedStackedBarRenderer)) { 362 return false; 363 } 364 GroupedStackedBarRenderer that = (GroupedStackedBarRenderer) obj; 365 if (!this.seriesToGroupMap.equals(that.seriesToGroupMap)) { 366 return false; 367 } 368 return super.equals(obj); 369 } 370 371}