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