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