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 */
084 public class StackedXYAreaRenderer2 extends XYAreaRenderer2
085 implements Cloneable, PublicCloneable, 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 PlotOrientation orientation = plot.getOrientation();
228
229 // get the data point...
230 double x1 = dataset.getXValue(series, item);
231 double y1 = dataset.getYValue(series, item);
232 if (Double.isNaN(y1)) {
233 y1 = 0.0;
234 }
235 double[] stack1 = getStackValues(tdataset, series, item);
236
237 // get the previous point and the next point so we can calculate a
238 // "hot spot" for the area (used by the chart entity)...
239 double x0 = dataset.getXValue(series, Math.max(item - 1, 0));
240 double y0 = dataset.getYValue(series, Math.max(item - 1, 0));
241 if (Double.isNaN(y0)) {
242 y0 = 0.0;
243 }
244 double[] stack0 = getStackValues(tdataset, series, Math.max(item - 1,
245 0));
246
247 int itemCount = dataset.getItemCount(series);
248 double x2 = dataset.getXValue(series, Math.min(item + 1,
249 itemCount - 1));
250 double y2 = dataset.getYValue(series, Math.min(item + 1,
251 itemCount - 1));
252 if (Double.isNaN(y2)) {
253 y2 = 0.0;
254 }
255 double[] stack2 = getStackValues(tdataset, series, Math.min(item + 1,
256 itemCount - 1));
257
258 double xleft = (x0 + x1) / 2.0;
259 double xright = (x1 + x2) / 2.0;
260 double[] stackLeft = averageStackValues(stack0, stack1);
261 double[] stackRight = averageStackValues(stack1, stack2);
262 double[] adjStackLeft = adjustedStackValues(stack0, stack1);
263 double[] adjStackRight = adjustedStackValues(stack1, stack2);
264
265 RectangleEdge edge0 = plot.getDomainAxisEdge();
266
267 float transX1 = (float) domainAxis.valueToJava2D(x1, dataArea, edge0);
268 float transXLeft = (float) domainAxis.valueToJava2D(xleft, dataArea,
269 edge0);
270 float transXRight = (float) domainAxis.valueToJava2D(xright, dataArea,
271 edge0);
272
273 if (this.roundXCoordinates) {
274 transX1 = Math.round(transX1);
275 transXLeft = Math.round(transXLeft);
276 transXRight = Math.round(transXRight);
277 }
278 float transY1;
279
280 RectangleEdge edge1 = plot.getRangeAxisEdge();
281
282 GeneralPath left = new GeneralPath();
283 GeneralPath right = new GeneralPath();
284 if (y1 >= 0.0) { // handle positive value
285 transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[1], dataArea,
286 edge1);
287 float transStack1 = (float) rangeAxis.valueToJava2D(stack1[1],
288 dataArea, edge1);
289 float transStackLeft = (float) rangeAxis.valueToJava2D(
290 adjStackLeft[1], dataArea, edge1);
291
292 // LEFT POLYGON
293 if (y0 >= 0.0) {
294 double yleft = (y0 + y1) / 2.0 + stackLeft[1];
295 float transYLeft
296 = (float) rangeAxis.valueToJava2D(yleft, dataArea, edge1);
297 if (orientation == PlotOrientation.VERTICAL) {
298 left.moveTo(transX1, transY1);
299 left.lineTo(transX1, transStack1);
300 left.lineTo(transXLeft, transStackLeft);
301 left.lineTo(transXLeft, transYLeft);
302 }
303 else {
304 left.moveTo(transY1, transX1);
305 left.lineTo(transStack1, transX1);
306 left.lineTo(transStackLeft, transXLeft);
307 left.lineTo(transYLeft, transXLeft);
308 }
309 left.closePath();
310 }
311 else {
312 if (orientation == PlotOrientation.VERTICAL) {
313 left.moveTo(transX1, transStack1);
314 left.lineTo(transX1, transY1);
315 left.lineTo(transXLeft, transStackLeft);
316 }
317 else {
318 left.moveTo(transStack1, transX1);
319 left.lineTo(transY1, transX1);
320 left.lineTo(transStackLeft, transXLeft);
321 }
322 left.closePath();
323 }
324
325 float transStackRight = (float) rangeAxis.valueToJava2D(
326 adjStackRight[1], dataArea, edge1);
327 // RIGHT POLYGON
328 if (y2 >= 0.0) {
329 double yright = (y1 + y2) / 2.0 + stackRight[1];
330 float transYRight
331 = (float) rangeAxis.valueToJava2D(yright, dataArea, edge1);
332 if (orientation == PlotOrientation.VERTICAL) {
333 right.moveTo(transX1, transStack1);
334 right.lineTo(transX1, transY1);
335 right.lineTo(transXRight, transYRight);
336 right.lineTo(transXRight, transStackRight);
337 }
338 else {
339 right.moveTo(transStack1, transX1);
340 right.lineTo(transY1, transX1);
341 right.lineTo(transYRight, transXRight);
342 right.lineTo(transStackRight, transXRight);
343 }
344 right.closePath();
345 }
346 else {
347 if (orientation == PlotOrientation.VERTICAL) {
348 right.moveTo(transX1, transStack1);
349 right.lineTo(transX1, transY1);
350 right.lineTo(transXRight, transStackRight);
351 }
352 else {
353 right.moveTo(transStack1, transX1);
354 right.lineTo(transY1, transX1);
355 right.lineTo(transStackRight, transXRight);
356 }
357 right.closePath();
358 }
359 }
360 else { // handle negative value
361 transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[0], dataArea,
362 edge1);
363 float transStack1 = (float) rangeAxis.valueToJava2D(stack1[0],
364 dataArea, edge1);
365 float transStackLeft = (float) rangeAxis.valueToJava2D(
366 adjStackLeft[0], dataArea, edge1);
367
368 // LEFT POLYGON
369 if (y0 >= 0.0) {
370 if (orientation == PlotOrientation.VERTICAL) {
371 left.moveTo(transX1, transStack1);
372 left.lineTo(transX1, transY1);
373 left.lineTo(transXLeft, transStackLeft);
374 }
375 else {
376 left.moveTo(transStack1, transX1);
377 left.lineTo(transY1, transX1);
378 left.lineTo(transStackLeft, transXLeft);
379 }
380 left.clone();
381 }
382 else {
383 double yleft = (y0 + y1) / 2.0 + stackLeft[0];
384 float transYLeft = (float) rangeAxis.valueToJava2D(yleft,
385 dataArea, edge1);
386 if (orientation == PlotOrientation.VERTICAL) {
387 left.moveTo(transX1, transY1);
388 left.lineTo(transX1, transStack1);
389 left.lineTo(transXLeft, transStackLeft);
390 left.lineTo(transXLeft, transYLeft);
391 }
392 else {
393 left.moveTo(transY1, transX1);
394 left.lineTo(transStack1, transX1);
395 left.lineTo(transStackLeft, transXLeft);
396 left.lineTo(transYLeft, transXLeft);
397 }
398 left.closePath();
399 }
400 float transStackRight = (float) rangeAxis.valueToJava2D(
401 adjStackRight[0], dataArea, edge1);
402
403 // RIGHT POLYGON
404 if (y2 >= 0.0) {
405 if (orientation == PlotOrientation.VERTICAL) {
406 right.moveTo(transX1, transStack1);
407 right.lineTo(transX1, transY1);
408 right.lineTo(transXRight, transStackRight);
409 }
410 else {
411 right.moveTo(transStack1, transX1);
412 right.lineTo(transY1, transX1);
413 right.lineTo(transStackRight, transXRight);
414 }
415 right.closePath();
416 }
417 else {
418 double yright = (y1 + y2) / 2.0 + stackRight[0];
419 float transYRight = (float) rangeAxis.valueToJava2D(yright,
420 dataArea, edge1);
421 if (orientation == PlotOrientation.VERTICAL) {
422 right.moveTo(transX1, transStack1);
423 right.lineTo(transX1, transY1);
424 right.lineTo(transXRight, transYRight);
425 right.lineTo(transXRight, transStackRight);
426 }
427 else {
428 right.moveTo(transStack1, transX1);
429 right.lineTo(transY1, transX1);
430 right.lineTo(transYRight, transXRight);
431 right.lineTo(transStackRight, transXRight);
432 }
433 right.closePath();
434 }
435 }
436
437 // Get series Paint and Stroke
438 Paint itemPaint = getItemPaint(series, item);
439 if (pass == 0) {
440 g2.setPaint(itemPaint);
441 g2.fill(left);
442 g2.fill(right);
443 }
444
445 // add an entity for the item...
446 if (entities != null) {
447 GeneralPath gp = new GeneralPath(left);
448 gp.append(right, false);
449 entityArea = gp;
450 addEntity(entities, entityArea, dataset, series, item,
451 transX1, transY1);
452 }
453
454 }
455
456 /**
457 * Calculates the stacked values (one positive and one negative) of all
458 * series up to, but not including, <code>series</code> for the specified
459 * item. It returns [0.0, 0.0] if <code>series</code> is the first series.
460 *
461 * @param dataset the dataset (<code>null</code> not permitted).
462 * @param series the series index.
463 * @param index the item index.
464 *
465 * @return An array containing the cumulative negative and positive values
466 * for all series values up to but excluding <code>series</code>
467 * for <code>index</code>.
468 */
469 private double[] getStackValues(TableXYDataset dataset,
470 int series, int index) {
471 double[] result = new double[2];
472 for (int i = 0; i < series; i++) {
473 double v = dataset.getYValue(i, index);
474 if (!Double.isNaN(v)) {
475 if (v >= 0.0) {
476 result[1] += v;
477 }
478 else {
479 result[0] += v;
480 }
481 }
482 }
483 return result;
484 }
485
486 /**
487 * Returns a pair of "stack" values calculated as the mean of the two
488 * specified stack value pairs.
489 *
490 * @param stack1 the first stack pair.
491 * @param stack2 the second stack pair.
492 *
493 * @return A pair of average stack values.
494 */
495 private double[] averageStackValues(double[] stack1, double[] stack2) {
496 double[] result = new double[2];
497 result[0] = (stack1[0] + stack2[0]) / 2.0;
498 result[1] = (stack1[1] + stack2[1]) / 2.0;
499 return result;
500 }
501
502 /**
503 * Calculates adjusted stack values from the supplied values. The value is
504 * the mean of the supplied values, unless either of the supplied values
505 * is zero, in which case the adjusted value is zero also.
506 *
507 * @param stack1 the first stack pair.
508 * @param stack2 the second stack pair.
509 *
510 * @return A pair of average stack values.
511 */
512 private double[] adjustedStackValues(double[] stack1, double[] stack2) {
513 double[] result = new double[2];
514 if (stack1[0] == 0.0 || stack2[0] == 0.0) {
515 result[0] = 0.0;
516 }
517 else {
518 result[0] = (stack1[0] + stack2[0]) / 2.0;
519 }
520 if (stack1[1] == 0.0 || stack2[1] == 0.0) {
521 result[1] = 0.0;
522 }
523 else {
524 result[1] = (stack1[1] + stack2[1]) / 2.0;
525 }
526 return result;
527 }
528
529 /**
530 * Tests this renderer for equality with an arbitrary object.
531 *
532 * @param obj the object (<code>null</code> permitted).
533 *
534 * @return A boolean.
535 */
536 public boolean equals(Object obj) {
537 if (obj == this) {
538 return true;
539 }
540 if (!(obj instanceof StackedXYAreaRenderer2)) {
541 return false;
542 }
543 StackedXYAreaRenderer2 that = (StackedXYAreaRenderer2) obj;
544 if (this.roundXCoordinates != that.roundXCoordinates) {
545 return false;
546 }
547 return super.equals(obj);
548 }
549
550 /**
551 * Returns a clone of the renderer.
552 *
553 * @return A clone.
554 *
555 * @throws CloneNotSupportedException if the renderer cannot be cloned.
556 */
557 public Object clone() throws CloneNotSupportedException {
558 return super.clone();
559 }
560
561 }