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 * StackedXYAreaRenderer2.java
029 * ---------------------------
030 * (C) Copyright 2004-2007, by Object Refinery Limited and Contributors.
031 *
032 * Original Author: David Gilbert (for Object Refinery Limited), based on
033 * the StackedXYAreaRenderer class by Richard Atkinson;
034 * Contributor(s): -;
035 *
036 * Changes:
037 * --------
038 * 30-Apr-2004 : Version 1 (DG);
039 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
040 * getYValue() (DG);
041 * 10-Sep-2004 : Removed getRangeType() method (DG);
042 * 06-Jan-2004 : Renamed getRangeExtent() --> findRangeBounds (DG);
043 * 28-Mar-2005 : Use getXValue() and getYValue() from dataset (DG);
044 * 03-Oct-2005 : Add entity generation to drawItem() method (DG);
045 * ------------- JFREECHART 1.0.x ---------------------------------------------
046 * 22-Aug-2006 : Handle null and empty datasets correctly in the
047 * findRangeBounds() method (DG);
048 * 22-Sep-2006 : Added a flag to allow rounding of x-coordinates (after
049 * translation to Java2D space) in order to avoid the striping
050 * that can result from anti-aliasing (thanks to Doug
051 * Clayton) (DG);
052 * 30-Nov-2006 : Added accessor methods for the roundXCoordinates flag (DG);
053 *
054 */
055
056 package org.jfree.chart.renderer.xy;
057
058 import java.awt.Graphics2D;
059 import java.awt.Paint;
060 import java.awt.Shape;
061 import java.awt.geom.GeneralPath;
062 import java.awt.geom.Rectangle2D;
063 import java.io.Serializable;
064
065 import org.jfree.chart.axis.ValueAxis;
066 import org.jfree.chart.entity.EntityCollection;
067 import org.jfree.chart.event.RendererChangeEvent;
068 import org.jfree.chart.labels.XYToolTipGenerator;
069 import org.jfree.chart.plot.CrosshairState;
070 import org.jfree.chart.plot.PlotRenderingInfo;
071 import org.jfree.chart.plot.XYPlot;
072 import org.jfree.chart.urls.XYURLGenerator;
073 import org.jfree.data.Range;
074 import org.jfree.data.xy.TableXYDataset;
075 import org.jfree.data.xy.XYDataset;
076 import org.jfree.ui.RectangleEdge;
077 import org.jfree.util.PublicCloneable;
078
079 /**
080 * A stacked area renderer for the {@link XYPlot} class.
081 */
082 public class StackedXYAreaRenderer2 extends XYAreaRenderer2
083 implements Cloneable,
084 PublicCloneable,
085 Serializable {
086
087 /** For serialization. */
088 private static final long serialVersionUID = 7752676509764539182L;
089
090 /**
091 * This flag controls whether or not the x-coordinates (in Java2D space)
092 * are rounded to integers. When set to true, this can avoid the vertical
093 * striping that anti-aliasing can generate. However, the rounding may not
094 * be appropriate for output in high resolution formats (for example,
095 * vector graphics formats such as SVG and PDF).
096 *
097 * @since 1.0.3
098 */
099 private boolean roundXCoordinates;
100
101 /**
102 * Creates a new renderer.
103 */
104 public StackedXYAreaRenderer2() {
105 this(null, null);
106 }
107
108 /**
109 * Constructs a new renderer.
110 *
111 * @param labelGenerator the tool tip generator to use. <code>null</code>
112 * is none.
113 * @param urlGenerator the URL generator (<code>null</code> permitted).
114 */
115 public StackedXYAreaRenderer2(XYToolTipGenerator labelGenerator,
116 XYURLGenerator urlGenerator) {
117 super(labelGenerator, urlGenerator);
118 this.roundXCoordinates = true;
119 }
120
121 /**
122 * Returns the flag that controls whether or not the x-coordinates (in
123 * Java2D space) are rounded to integer values.
124 *
125 * @return The flag.
126 *
127 * @since 1.0.4
128 *
129 * @see #setRoundXCoordinates(boolean)
130 */
131 public boolean getRoundXCoordinates() {
132 return this.roundXCoordinates;
133 }
134
135 /**
136 * Sets the flag that controls whether or not the x-coordinates (in
137 * Java2D space) are rounded to integer values, and sends a
138 * {@link RendererChangeEvent} to all registered listeners.
139 *
140 * @param round the new flag value.
141 *
142 * @since 1.0.4
143 *
144 * @see #getRoundXCoordinates()
145 */
146 public void setRoundXCoordinates(boolean round) {
147 this.roundXCoordinates = round;
148 fireChangeEvent();
149 }
150
151 /**
152 * Returns the range of values the renderer requires to display all the
153 * items from the specified dataset.
154 *
155 * @param dataset the dataset (<code>null</code> permitted).
156 *
157 * @return The range (or <code>null</code> if the dataset is
158 * <code>null</code> or empty).
159 */
160 public Range findRangeBounds(XYDataset dataset) {
161 if (dataset == null) {
162 return null;
163 }
164 double min = Double.POSITIVE_INFINITY;
165 double max = Double.NEGATIVE_INFINITY;
166 TableXYDataset d = (TableXYDataset) dataset;
167 int itemCount = d.getItemCount();
168 for (int i = 0; i < itemCount; i++) {
169 double[] stackValues = getStackValues((TableXYDataset) dataset,
170 d.getSeriesCount(), i);
171 min = Math.min(min, stackValues[0]);
172 max = Math.max(max, stackValues[1]);
173 }
174 if (min == Double.POSITIVE_INFINITY) {
175 return null;
176 }
177 return new Range(min, max);
178 }
179
180 /**
181 * Returns the number of passes required by the renderer.
182 *
183 * @return 1.
184 */
185 public int getPassCount() {
186 return 1;
187 }
188
189 /**
190 * Draws the visual representation of a single data item.
191 *
192 * @param g2 the graphics device.
193 * @param state the renderer state.
194 * @param dataArea the area within which the data is being drawn.
195 * @param info collects information about the drawing.
196 * @param plot the plot (can be used to obtain standard color information
197 * etc).
198 * @param domainAxis the domain axis.
199 * @param rangeAxis the range axis.
200 * @param dataset the dataset.
201 * @param series the series index (zero-based).
202 * @param item the item index (zero-based).
203 * @param crosshairState information about crosshairs on a plot.
204 * @param pass the pass index.
205 */
206 public void drawItem(Graphics2D g2,
207 XYItemRendererState state,
208 Rectangle2D dataArea,
209 PlotRenderingInfo info,
210 XYPlot plot,
211 ValueAxis domainAxis,
212 ValueAxis rangeAxis,
213 XYDataset dataset,
214 int series,
215 int item,
216 CrosshairState crosshairState,
217 int pass) {
218
219 // setup for collecting optional entity info...
220 Shape entityArea = null;
221 EntityCollection entities = null;
222 if (info != null) {
223 entities = info.getOwner().getEntityCollection();
224 }
225
226 TableXYDataset tdataset = (TableXYDataset) dataset;
227
228 // get the data point...
229 double x1 = dataset.getXValue(series, item);
230 double y1 = dataset.getYValue(series, item);
231 if (Double.isNaN(y1)) {
232 y1 = 0.0;
233 }
234 double[] stack1 = getStackValues(tdataset, series, item);
235
236 // get the previous point and the next point so we can calculate a
237 // "hot spot" for the area (used by the chart entity)...
238 double x0 = dataset.getXValue(series, Math.max(item - 1, 0));
239 double y0 = dataset.getYValue(series, Math.max(item - 1, 0));
240 if (Double.isNaN(y0)) {
241 y0 = 0.0;
242 }
243 double[] stack0 = getStackValues(tdataset, series, Math.max(item - 1,
244 0));
245
246 int itemCount = dataset.getItemCount(series);
247 double x2 = dataset.getXValue(series, Math.min(item + 1,
248 itemCount - 1));
249 double y2 = dataset.getYValue(series, Math.min(item + 1,
250 itemCount - 1));
251 if (Double.isNaN(y2)) {
252 y2 = 0.0;
253 }
254 double[] stack2 = getStackValues(tdataset, series, Math.min(item + 1,
255 itemCount - 1));
256
257 double xleft = (x0 + x1) / 2.0;
258 double xright = (x1 + x2) / 2.0;
259 double[] stackLeft = averageStackValues(stack0, stack1);
260 double[] stackRight = averageStackValues(stack1, stack2);
261 double[] adjStackLeft = adjustedStackValues(stack0, stack1);
262 double[] adjStackRight = adjustedStackValues(stack1, stack2);
263
264 RectangleEdge edge0 = plot.getDomainAxisEdge();
265
266 float transX1 = (float) domainAxis.valueToJava2D(x1, dataArea, edge0);
267 float transXLeft = (float) domainAxis.valueToJava2D(xleft, dataArea,
268 edge0);
269 float transXRight = (float) domainAxis.valueToJava2D(xright, dataArea,
270 edge0);
271
272 if (this.roundXCoordinates) {
273 transX1 = Math.round(transX1);
274 transXLeft = Math.round(transXLeft);
275 transXRight = Math.round(transXRight);
276 }
277 float transY1;
278
279 RectangleEdge edge1 = plot.getRangeAxisEdge();
280
281 GeneralPath left = new GeneralPath();
282 GeneralPath right = new GeneralPath();
283 if (y1 >= 0.0) { // handle positive value
284 transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[1], dataArea,
285 edge1);
286 float transStack1 = (float) rangeAxis.valueToJava2D(stack1[1],
287 dataArea, edge1);
288 float transStackLeft = (float) rangeAxis.valueToJava2D(
289 adjStackLeft[1], dataArea, edge1);
290
291 // LEFT POLYGON
292 if (y0 >= 0.0) {
293 double yleft = (y0 + y1) / 2.0 + stackLeft[1];
294 float transYLeft
295 = (float) rangeAxis.valueToJava2D(yleft, dataArea, edge1);
296 left.moveTo(transX1, transY1);
297 left.lineTo(transX1, transStack1);
298 left.lineTo(transXLeft, transStackLeft);
299 left.lineTo(transXLeft, transYLeft);
300 left.closePath();
301 }
302 else {
303 left.moveTo(transX1, transStack1);
304 left.lineTo(transX1, transY1);
305 left.lineTo(transXLeft, transStackLeft);
306 left.closePath();
307 }
308
309 float transStackRight = (float) rangeAxis.valueToJava2D(
310 adjStackRight[1], dataArea, edge1);
311 // RIGHT POLYGON
312 if (y2 >= 0.0) {
313 double yright = (y1 + y2) / 2.0 + stackRight[1];
314 float transYRight
315 = (float) rangeAxis.valueToJava2D(yright, dataArea, edge1);
316 right.moveTo(transX1, transStack1);
317 right.lineTo(transX1, transY1);
318 right.lineTo(transXRight, transYRight);
319 right.lineTo(transXRight, transStackRight);
320 right.closePath();
321 }
322 else {
323 right.moveTo(transX1, transStack1);
324 right.lineTo(transX1, transY1);
325 right.lineTo(transXRight, transStackRight);
326 right.closePath();
327 }
328 }
329 else { // handle negative value
330 transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[0], dataArea,
331 edge1);
332 float transStack1 = (float) rangeAxis.valueToJava2D(stack1[0],
333 dataArea, edge1);
334 float transStackLeft = (float) rangeAxis.valueToJava2D(
335 adjStackLeft[0], dataArea, edge1);
336
337 // LEFT POLYGON
338 if (y0 >= 0.0) {
339 left.moveTo(transX1, transStack1);
340 left.lineTo(transX1, transY1);
341 left.lineTo(transXLeft, transStackLeft);
342 left.clone();
343 }
344 else {
345 double yleft = (y0 + y1) / 2.0 + stackLeft[0];
346 float transYLeft = (float) rangeAxis.valueToJava2D(yleft,
347 dataArea, edge1);
348 left.moveTo(transX1, transY1);
349 left.lineTo(transX1, transStack1);
350 left.lineTo(transXLeft, transStackLeft);
351 left.lineTo(transXLeft, transYLeft);
352 left.closePath();
353 }
354 float transStackRight = (float) rangeAxis.valueToJava2D(
355 adjStackRight[0], dataArea, edge1);
356
357 // RIGHT POLYGON
358 if (y2 >= 0.0) {
359 right.moveTo(transX1, transStack1);
360 right.lineTo(transX1, transY1);
361 right.lineTo(transXRight, transStackRight);
362 right.closePath();
363 }
364 else {
365 double yright = (y1 + y2) / 2.0 + stackRight[0];
366 float transYRight = (float) rangeAxis.valueToJava2D(yright,
367 dataArea, edge1);
368 right.moveTo(transX1, transStack1);
369 right.lineTo(transX1, transY1);
370 right.lineTo(transXRight, transYRight);
371 right.lineTo(transXRight, transStackRight);
372 right.closePath();
373 }
374 }
375
376 // Get series Paint and Stroke
377 Paint itemPaint = getItemPaint(series, item);
378 if (pass == 0) {
379 g2.setPaint(itemPaint);
380 g2.fill(left);
381 g2.fill(right);
382 }
383
384 // add an entity for the item...
385 if (entities != null) {
386 GeneralPath gp = new GeneralPath(left);
387 gp.append(right, false);
388 entityArea = gp;
389 addEntity(entities, entityArea, dataset, series, item,
390 transX1, transY1);
391 }
392
393 }
394
395 /**
396 * Calculates the stacked values (one positive and one negative) of all
397 * series up to, but not including, <code>series</code> for the specified
398 * item. It returns [0.0, 0.0] if <code>series</code> is the first series.
399 *
400 * @param dataset the dataset (<code>null</code> not permitted).
401 * @param series the series index.
402 * @param index the item index.
403 *
404 * @return An array containing the cumulative negative and positive values
405 * for all series values up to but excluding <code>series</code>
406 * for <code>index</code>.
407 */
408 private double[] getStackValues(TableXYDataset dataset,
409 int series, int index) {
410 double[] result = new double[2];
411 for (int i = 0; i < series; i++) {
412 double v = dataset.getYValue(i, index);
413 if (!Double.isNaN(v)) {
414 if (v >= 0.0) {
415 result[1] += v;
416 }
417 else {
418 result[0] += v;
419 }
420 }
421 }
422 return result;
423 }
424
425 /**
426 * Returns a pair of "stack" values calculated as the mean of the two
427 * specified stack value pairs.
428 *
429 * @param stack1 the first stack pair.
430 * @param stack2 the second stack pair.
431 *
432 * @return A pair of average stack values.
433 */
434 private double[] averageStackValues(double[] stack1, double[] stack2) {
435 double[] result = new double[2];
436 result[0] = (stack1[0] + stack2[0]) / 2.0;
437 result[1] = (stack1[1] + stack2[1]) / 2.0;
438 return result;
439 }
440
441 /**
442 * Calculates adjusted stack values from the supplied values. The value is
443 * the mean of the supplied values, unless either of the supplied values
444 * is zero, in which case the adjusted value is zero also.
445 *
446 * @param stack1 the first stack pair.
447 * @param stack2 the second stack pair.
448 *
449 * @return A pair of average stack values.
450 */
451 private double[] adjustedStackValues(double[] stack1, double[] stack2) {
452 double[] result = new double[2];
453 if (stack1[0] == 0.0 || stack2[0] == 0.0) {
454 result[0] = 0.0;
455 }
456 else {
457 result[0] = (stack1[0] + stack2[0]) / 2.0;
458 }
459 if (stack1[1] == 0.0 || stack2[1] == 0.0) {
460 result[1] = 0.0;
461 }
462 else {
463 result[1] = (stack1[1] + stack2[1]) / 2.0;
464 }
465 return result;
466 }
467
468 /**
469 * Tests this renderer for equality with an arbitrary object.
470 *
471 * @param obj the object (<code>null</code> permitted).
472 *
473 * @return A boolean.
474 */
475 public boolean equals(Object obj) {
476 if (obj == this) {
477 return true;
478 }
479 if (!(obj instanceof StackedXYAreaRenderer2)) {
480 return false;
481 }
482 StackedXYAreaRenderer2 that = (StackedXYAreaRenderer2) obj;
483 if (this.roundXCoordinates != that.roundXCoordinates) {
484 return false;
485 }
486 return super.equals(obj);
487 }
488
489 /**
490 * Returns a clone of the renderer.
491 *
492 * @return A clone.
493 *
494 * @throws CloneNotSupportedException if the renderer cannot be cloned.
495 */
496 public Object clone() throws CloneNotSupportedException {
497 return super.clone();
498 }
499
500 }