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