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