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: * StackedXYBarRenderer.java
029: * -------------------------
030: * (C) Copyright 2004-2007, by Andreas Schroeder and Contributors.
031: *
032: * Original Author: Andreas Schroeder;
033: * Contributor(s): David Gilbert (for Object Refinery Limited);
034: *
035: * $Id: StackedXYBarRenderer.java,v 1.10.2.5 2007/03/21 10:04:20 mungady Exp $
036: *
037: * Changes
038: * -------
039: * 01-Apr-2004 : Version 1 (AS);
040: * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
041: * getYValue() (DG);
042: * 15-Aug-2004 : Added drawBarOutline to control draw/don't-draw bar
043: * outlines (BN);
044: * 10-Sep-2004 : drawBarOutline attribute is now inherited from XYBarRenderer
045: * and double primitives are retrieved from the dataset rather
046: * than Number objects (DG);
047: * 07-Jan-2005 : Updated for method name change in DatasetUtilities (DG);
048: * 25-Jan-2005 : Modified to handle negative values correctly (DG);
049: * ------------- JFREECHART 1.0.x ---------------------------------------------
050: * 06-Dec-2006 : Added support for GradientPaint (DG);
051: * 15-Mar-2007 : Added renderAsPercentages option (DG);
052: *
053: */
054:
055: package org.jfree.chart.renderer.xy;
056:
057: import java.awt.GradientPaint;
058: import java.awt.Graphics2D;
059: import java.awt.Paint;
060: import java.awt.geom.Rectangle2D;
061:
062: import org.jfree.chart.axis.ValueAxis;
063: import org.jfree.chart.entity.EntityCollection;
064: import org.jfree.chart.event.RendererChangeEvent;
065: import org.jfree.chart.labels.ItemLabelAnchor;
066: import org.jfree.chart.labels.ItemLabelPosition;
067: import org.jfree.chart.labels.XYItemLabelGenerator;
068: import org.jfree.chart.plot.CrosshairState;
069: import org.jfree.chart.plot.PlotOrientation;
070: import org.jfree.chart.plot.PlotRenderingInfo;
071: import org.jfree.chart.plot.XYPlot;
072: import org.jfree.data.Range;
073: import org.jfree.data.general.DatasetUtilities;
074: import org.jfree.data.xy.IntervalXYDataset;
075: import org.jfree.data.xy.TableXYDataset;
076: import org.jfree.data.xy.XYDataset;
077: import org.jfree.ui.RectangleEdge;
078: import org.jfree.ui.TextAnchor;
079:
080: /**
081: * A bar renderer that displays the series items stacked.
082: * The dataset used together with this renderer must be a
083: * {@link org.jfree.data.xy.IntervalXYDataset} and a
084: * {@link org.jfree.data.xy.TableXYDataset}. For example, the
085: * dataset class {@link org.jfree.data.xy.CategoryTableXYDataset}
086: * implements both interfaces.
087: */
088: public class StackedXYBarRenderer extends XYBarRenderer {
089:
090: /** For serialization. */
091: private static final long serialVersionUID = -7049101055533436444L;
092:
093: /** A flag that controls whether the bars display values or percentages. */
094: private boolean renderAsPercentages;
095:
096: /**
097: * Creates a new renderer.
098: */
099: public StackedXYBarRenderer() {
100: this (0.0);
101: }
102:
103: /**
104: * Creates a new renderer.
105: *
106: * @param margin the percentual amount of the bars that are cut away.
107: */
108: public StackedXYBarRenderer(double margin) {
109: super (margin);
110: this .renderAsPercentages = false;
111:
112: // set the default item label positions, which will only be used if
113: // the user requests visible item labels...
114: ItemLabelPosition p = new ItemLabelPosition(
115: ItemLabelAnchor.CENTER, TextAnchor.CENTER);
116: setBasePositiveItemLabelPosition(p);
117: setBaseNegativeItemLabelPosition(p);
118: setPositiveItemLabelPositionFallback(null);
119: setNegativeItemLabelPositionFallback(null);
120: }
121:
122: /**
123: * Returns <code>true</code> if the renderer displays each item value as
124: * a percentage (so that the stacked bars add to 100%), and
125: * <code>false</code> otherwise.
126: *
127: * @return A boolean.
128: *
129: * @see #setRenderAsPercentages(boolean)
130: *
131: * @since 1.0.5
132: */
133: public boolean getRenderAsPercentages() {
134: return this .renderAsPercentages;
135: }
136:
137: /**
138: * Sets the flag that controls whether the renderer displays each item
139: * value as a percentage (so that the stacked bars add to 100%), and sends
140: * a {@link RendererChangeEvent} to all registered listeners.
141: *
142: * @param asPercentages the flag.
143: *
144: * @see #getRenderAsPercentages()
145: *
146: * @since 1.0.5
147: */
148: public void setRenderAsPercentages(boolean asPercentages) {
149: this .renderAsPercentages = asPercentages;
150: notifyListeners(new RendererChangeEvent(this ));
151: }
152:
153: /**
154: * Returns <code>2</code> to indicate that this renderer requires two
155: * passes for drawing (item labels are drawn in the second pass so that
156: * they always appear in front of all the bars).
157: *
158: * @return <code>2</code>.
159: */
160: public int getPassCount() {
161: return 2;
162: }
163:
164: /**
165: * Initialises the renderer and returns a state object that should be
166: * passed to all subsequent calls to the drawItem() method. Here there is
167: * nothing to do.
168: *
169: * @param g2 the graphics device.
170: * @param dataArea the area inside the axes.
171: * @param plot the plot.
172: * @param data the data.
173: * @param info an optional info collection object to return data back to
174: * the caller.
175: *
176: * @return A state object.
177: */
178: public XYItemRendererState initialise(Graphics2D g2,
179: Rectangle2D dataArea, XYPlot plot, XYDataset data,
180: PlotRenderingInfo info) {
181: return new XYBarRendererState(info);
182: }
183:
184: /**
185: * Returns the range of values the renderer requires to display all the
186: * items from the specified dataset.
187: *
188: * @param dataset the dataset (<code>null</code> permitted).
189: *
190: * @return The range (<code>null</code> if the dataset is <code>null</code>
191: * or empty).
192: */
193: public Range findRangeBounds(XYDataset dataset) {
194: if (dataset != null) {
195: if (this .renderAsPercentages) {
196: return new Range(0.0, 1.0);
197: } else {
198: return DatasetUtilities
199: .findStackedRangeBounds((TableXYDataset) dataset);
200: }
201: } else {
202: return null;
203: }
204: }
205:
206: /**
207: * Draws the visual representation of a single data item.
208: *
209: * @param g2 the graphics device.
210: * @param state the renderer state.
211: * @param dataArea the area within which the plot is being drawn.
212: * @param info collects information about the drawing.
213: * @param plot the plot (can be used to obtain standard color information
214: * etc).
215: * @param domainAxis the domain axis.
216: * @param rangeAxis the range axis.
217: * @param dataset the dataset.
218: * @param series the series index (zero-based).
219: * @param item the item index (zero-based).
220: * @param crosshairState crosshair information for the plot
221: * (<code>null</code> permitted).
222: * @param pass the pass index.
223: */
224: public void drawItem(Graphics2D g2, XYItemRendererState state,
225: Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
226: ValueAxis domainAxis, ValueAxis rangeAxis,
227: XYDataset dataset, int series, int item,
228: CrosshairState crosshairState, int pass) {
229:
230: if (!(dataset instanceof IntervalXYDataset && dataset instanceof TableXYDataset)) {
231: String message = "dataset (type "
232: + dataset.getClass().getName()
233: + ") has wrong type:";
234: boolean and = false;
235: if (!IntervalXYDataset.class.isAssignableFrom(dataset
236: .getClass())) {
237: message += " it is no IntervalXYDataset";
238: and = true;
239: }
240: if (!TableXYDataset.class.isAssignableFrom(dataset
241: .getClass())) {
242: if (and) {
243: message += " and";
244: }
245: message += " it is no TableXYDataset";
246: }
247:
248: throw new IllegalArgumentException(message);
249: }
250:
251: IntervalXYDataset intervalDataset = (IntervalXYDataset) dataset;
252: double value = intervalDataset.getYValue(series, item);
253: if (Double.isNaN(value)) {
254: return;
255: }
256:
257: // if we are rendering the values as percentages, we need to calculate
258: // the total for the current item. Unfortunately here we end up
259: // repeating the calculation more times than is strictly necessary -
260: // hopefully I'll come back to this and find a way to add the
261: // total(s) to the renderer state. The other problem is we implicitly
262: // assume the dataset has no negative values...perhaps that can be
263: // fixed too.
264: double total = 0.0;
265: if (this .renderAsPercentages) {
266: total = DatasetUtilities.calculateStackTotal(
267: (TableXYDataset) dataset, item);
268: value = value / total;
269: }
270:
271: double positiveBase = 0.0;
272: double negativeBase = 0.0;
273:
274: for (int i = 0; i < series; i++) {
275: double v = dataset.getYValue(i, item);
276: if (!Double.isNaN(v)) {
277: if (this .renderAsPercentages) {
278: v = v / total;
279: }
280: if (v > 0) {
281: positiveBase = positiveBase + v;
282: } else {
283: negativeBase = negativeBase + v;
284: }
285: }
286: }
287:
288: double translatedBase;
289: double translatedValue;
290: RectangleEdge edgeR = plot.getRangeAxisEdge();
291: if (value > 0.0) {
292: translatedBase = rangeAxis.valueToJava2D(positiveBase,
293: dataArea, edgeR);
294: translatedValue = rangeAxis.valueToJava2D(positiveBase
295: + value, dataArea, edgeR);
296: } else {
297: translatedBase = rangeAxis.valueToJava2D(negativeBase,
298: dataArea, edgeR);
299: translatedValue = rangeAxis.valueToJava2D(negativeBase
300: + value, dataArea, edgeR);
301: }
302:
303: RectangleEdge edgeD = plot.getDomainAxisEdge();
304: double startX = intervalDataset.getStartXValue(series, item);
305: if (Double.isNaN(startX)) {
306: return;
307: }
308: double translatedStartX = domainAxis.valueToJava2D(startX,
309: dataArea, edgeD);
310:
311: double endX = intervalDataset.getEndXValue(series, item);
312: if (Double.isNaN(endX)) {
313: return;
314: }
315: double translatedEndX = domainAxis.valueToJava2D(endX,
316: dataArea, edgeD);
317:
318: double translatedWidth = Math.max(1, Math.abs(translatedEndX
319: - translatedStartX));
320: double translatedHeight = Math.abs(translatedValue
321: - translatedBase);
322: if (getMargin() > 0.0) {
323: double cut = translatedWidth * getMargin();
324: translatedWidth = translatedWidth - cut;
325: translatedStartX = translatedStartX + cut / 2;
326: }
327:
328: Rectangle2D bar = null;
329: PlotOrientation orientation = plot.getOrientation();
330: if (orientation == PlotOrientation.HORIZONTAL) {
331: bar = new Rectangle2D.Double(Math.min(translatedBase,
332: translatedValue), translatedEndX, translatedHeight,
333: translatedWidth);
334: } else if (orientation == PlotOrientation.VERTICAL) {
335: bar = new Rectangle2D.Double(translatedStartX, Math.min(
336: translatedBase, translatedValue), translatedWidth,
337: translatedHeight);
338: }
339:
340: if (pass == 0) {
341: Paint itemPaint = getItemPaint(series, item);
342: if (getGradientPaintTransformer() != null
343: && itemPaint instanceof GradientPaint) {
344: GradientPaint gp = (GradientPaint) itemPaint;
345: itemPaint = getGradientPaintTransformer().transform(gp,
346: bar);
347: }
348: g2.setPaint(itemPaint);
349: g2.fill(bar);
350: if (isDrawBarOutline()
351: && Math.abs(translatedEndX - translatedStartX) > 3) {
352: g2.setStroke(getItemStroke(series, item));
353: g2.setPaint(getItemOutlinePaint(series, item));
354: g2.draw(bar);
355: }
356:
357: // add an entity for the item...
358: if (info != null) {
359: EntityCollection entities = info.getOwner()
360: .getEntityCollection();
361: if (entities != null) {
362: addEntity(entities, bar, dataset, series, item, bar
363: .getCenterX(), bar.getCenterY());
364: }
365: }
366: } else if (pass == 1) {
367: // handle item label drawing, now that we know all the bars have
368: // been drawn...
369: if (isItemLabelVisible(series, item)) {
370: XYItemLabelGenerator generator = getItemLabelGenerator(
371: series, item);
372: drawItemLabel(g2, dataset, series, item, plot,
373: generator, bar, value < 0.0);
374: }
375: }
376:
377: }
378:
379: /**
380: * Tests this renderer for equality with an arbitrary object.
381: *
382: * @param obj the object (<code>null</code> permitted).
383: *
384: * @return A boolean.
385: */
386: public boolean equals(Object obj) {
387: if (obj == this ) {
388: return true;
389: }
390: if (!(obj instanceof StackedXYBarRenderer)) {
391: return false;
392: }
393: StackedXYBarRenderer that = (StackedXYBarRenderer) obj;
394: if (this .renderAsPercentages != that.renderAsPercentages) {
395: return false;
396: }
397: return super .equals(obj);
398: }
399:
400: /**
401: * Returns a hash code for this instance.
402: *
403: * @return A hash code.
404: */
405: public int hashCode() {
406: int result = super .hashCode();
407: result = result * 37 + (this .renderAsPercentages ? 1 : 0);
408: return result;
409: }
410:
411: }
|