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 * StackedXYAreaRenderer.java
029 * --------------------------
030 * (C) Copyright 2003-2007, by Richard Atkinson and Contributors.
031 *
032 * Original Author: Richard Atkinson;
033 * Contributor(s): Christian W. Zuckschwerdt;
034 * David Gilbert (for Object Refinery Limited);
035 *
036 * $Id: StackedXYAreaRenderer.java,v 1.12.2.13 2007/05/24 13:49:12 mungady Exp $
037 *
038 * Changes:
039 * --------
040 * 27-Jul-2003 : Initial version (RA);
041 * 30-Jul-2003 : Modified entity constructor (CZ);
042 * 18-Aug-2003 : Now handles null values (RA);
043 * 20-Aug-2003 : Implemented Cloneable, PublicCloneable and Serializable (DG);
044 * 22-Sep-2003 : Changed to be a two pass renderer with optional shape Paint
045 * and Stroke (RA);
046 * 07-Oct-2003 : Added renderer state (DG);
047 * 10-Feb-2004 : Updated state object and changed drawItem() method to make
048 * overriding easier (DG);
049 * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState. Renamed
050 * XYToolTipGenerator --> XYItemLabelGenerator (DG);
051 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
052 * getYValue() (DG);
053 * 10-Sep-2004 : Removed getRangeType() method (DG);
054 * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG);
055 * 06-Jan-2005 : Override equals() (DG);
056 * 07-Jan-2005 : Update for method name changes in DatasetUtilities (DG);
057 * 28-Mar-2005 : Use getXValue() and getYValue() from dataset (DG);
058 * 06-Jun-2005 : Fixed null pointer exception, plus problems with equals() and
059 * serialization (DG);
060 * ------------- JFREECHART 1.0.x ---------------------------------------------
061 * 10-Nov-2006 : Fixed bug 1593156, NullPointerException with line
062 * plotting (DG);
063 * 02-Feb-2007 : Fixed bug 1649686, crosshairs don't stack y-values (DG);
064 * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG);
065 * 22-Mar-2007 : Fire change events in setShapePaint() and setShapeStroke()
066 * methods (DG);
067 * 20-Apr-2007 : Updated getLegendItem() for renderer change (DG);
068 *
069 */
070
071 package org.jfree.chart.renderer.xy;
072
073 import java.awt.Graphics2D;
074 import java.awt.Paint;
075 import java.awt.Point;
076 import java.awt.Polygon;
077 import java.awt.Shape;
078 import java.awt.Stroke;
079 import java.awt.geom.Line2D;
080 import java.awt.geom.Rectangle2D;
081 import java.io.IOException;
082 import java.io.ObjectInputStream;
083 import java.io.ObjectOutputStream;
084 import java.io.Serializable;
085 import java.util.Stack;
086
087 import org.jfree.chart.axis.ValueAxis;
088 import org.jfree.chart.entity.EntityCollection;
089 import org.jfree.chart.entity.XYItemEntity;
090 import org.jfree.chart.event.RendererChangeEvent;
091 import org.jfree.chart.labels.XYToolTipGenerator;
092 import org.jfree.chart.plot.CrosshairState;
093 import org.jfree.chart.plot.PlotOrientation;
094 import org.jfree.chart.plot.PlotRenderingInfo;
095 import org.jfree.chart.plot.XYPlot;
096 import org.jfree.chart.urls.XYURLGenerator;
097 import org.jfree.data.Range;
098 import org.jfree.data.general.DatasetUtilities;
099 import org.jfree.data.xy.TableXYDataset;
100 import org.jfree.data.xy.XYDataset;
101 import org.jfree.io.SerialUtilities;
102 import org.jfree.util.ObjectUtilities;
103 import org.jfree.util.PaintUtilities;
104 import org.jfree.util.PublicCloneable;
105 import org.jfree.util.ShapeUtilities;
106
107 /**
108 * A stacked area renderer for the {@link XYPlot} class.
109 * <br><br>
110 * SPECIAL NOTE: This renderer does not currently handle negative data values
111 * correctly. This should get fixed at some point, but the current workaround
112 * is to use the {@link StackedXYAreaRenderer2} class instead.
113 */
114 public class StackedXYAreaRenderer extends XYAreaRenderer
115 implements Cloneable,
116 PublicCloneable,
117 Serializable {
118
119 /** For serialization. */
120 private static final long serialVersionUID = 5217394318178570889L;
121
122 /**
123 * A state object for use by this renderer.
124 */
125 static class StackedXYAreaRendererState extends XYItemRendererState {
126
127 /** The area for the current series. */
128 private Polygon seriesArea;
129
130 /** The line. */
131 private Line2D line;
132
133 /** The points from the last series. */
134 private Stack lastSeriesPoints;
135
136 /** The points for the current series. */
137 private Stack currentSeriesPoints;
138
139 /**
140 * Creates a new state for the renderer.
141 *
142 * @param info the plot rendering info.
143 */
144 public StackedXYAreaRendererState(PlotRenderingInfo info) {
145 super(info);
146 this.seriesArea = null;
147 this.line = new Line2D.Double();
148 this.lastSeriesPoints = new Stack();
149 this.currentSeriesPoints = new Stack();
150 }
151
152 /**
153 * Returns the series area.
154 *
155 * @return The series area.
156 */
157 public Polygon getSeriesArea() {
158 return this.seriesArea;
159 }
160
161 /**
162 * Sets the series area.
163 *
164 * @param area the area.
165 */
166 public void setSeriesArea(Polygon area) {
167 this.seriesArea = area;
168 }
169
170 /**
171 * Returns the working line.
172 *
173 * @return The working line.
174 */
175 public Line2D getLine() {
176 return this.line;
177 }
178
179 /**
180 * Returns the current series points.
181 *
182 * @return The current series points.
183 */
184 public Stack getCurrentSeriesPoints() {
185 return this.currentSeriesPoints;
186 }
187
188 /**
189 * Sets the current series points.
190 *
191 * @param points the points.
192 */
193 public void setCurrentSeriesPoints(Stack points) {
194 this.currentSeriesPoints = points;
195 }
196
197 /**
198 * Returns the last series points.
199 *
200 * @return The last series points.
201 */
202 public Stack getLastSeriesPoints() {
203 return this.lastSeriesPoints;
204 }
205
206 /**
207 * Sets the last series points.
208 *
209 * @param points the points.
210 */
211 public void setLastSeriesPoints(Stack points) {
212 this.lastSeriesPoints = points;
213 }
214
215 }
216
217 /**
218 * Custom Paint for drawing all shapes, if null defaults to series shapes
219 */
220 private transient Paint shapePaint = null;
221
222 /**
223 * Custom Stroke for drawing all shapes, if null defaults to series
224 * strokes.
225 */
226 private transient Stroke shapeStroke = null;
227
228 /**
229 * Creates a new renderer.
230 */
231 public StackedXYAreaRenderer() {
232 this(AREA);
233 }
234
235 /**
236 * Constructs a new renderer.
237 *
238 * @param type the type of the renderer.
239 */
240 public StackedXYAreaRenderer(int type) {
241 this(type, null, null);
242 }
243
244 /**
245 * Constructs a new renderer. To specify the type of renderer, use one of
246 * the constants: <code>SHAPES</code>, <code>LINES</code>,
247 * <code>SHAPES_AND_LINES</code>, <code>AREA</code> or
248 * <code>AREA_AND_SHAPES</code>.
249 *
250 * @param type the type of renderer.
251 * @param labelGenerator the tool tip generator to use (<code>null</code>
252 * is none).
253 * @param urlGenerator the URL generator (<code>null</code> permitted).
254 */
255 public StackedXYAreaRenderer(int type,
256 XYToolTipGenerator labelGenerator,
257 XYURLGenerator urlGenerator) {
258
259 super(type, labelGenerator, urlGenerator);
260 }
261
262 /**
263 * Returns the paint used for rendering shapes, or <code>null</code> if
264 * using series paints.
265 *
266 * @return The paint (possibly <code>null</code>).
267 *
268 * @see #setShapePaint(Paint)
269 */
270 public Paint getShapePaint() {
271 return this.shapePaint;
272 }
273
274 /**
275 * Sets the paint for rendering shapes and sends a
276 * {@link RendererChangeEvent} to all registered listeners.
277 *
278 * @param shapePaint the paint (<code>null</code> permitted).
279 *
280 * @see #getShapePaint()
281 */
282 public void setShapePaint(Paint shapePaint) {
283 this.shapePaint = shapePaint;
284 fireChangeEvent();
285 }
286
287 /**
288 * Returns the stroke used for rendering shapes, or <code>null</code> if
289 * using series strokes.
290 *
291 * @return The stroke (possibly <code>null</code>).
292 *
293 * @see #setShapeStroke(Stroke)
294 */
295 public Stroke getShapeStroke() {
296 return this.shapeStroke;
297 }
298
299 /**
300 * Sets the stroke for rendering shapes and sends a
301 * {@link RendererChangeEvent} to all registered listeners.
302 *
303 * @param shapeStroke the stroke (<code>null</code> permitted).
304 *
305 * @see #getShapeStroke()
306 */
307 public void setShapeStroke(Stroke shapeStroke) {
308 this.shapeStroke = shapeStroke;
309 fireChangeEvent();
310 }
311
312 /**
313 * Initialises the renderer. This method will be called before the first
314 * item is rendered, giving the renderer an opportunity to initialise any
315 * state information it wants to maintain.
316 *
317 * @param g2 the graphics device.
318 * @param dataArea the area inside the axes.
319 * @param plot the plot.
320 * @param data the data.
321 * @param info an optional info collection object to return data back to
322 * the caller.
323 *
324 * @return A state object that should be passed to subsequent calls to the
325 * drawItem() method.
326 */
327 public XYItemRendererState initialise(Graphics2D g2,
328 Rectangle2D dataArea,
329 XYPlot plot,
330 XYDataset data,
331 PlotRenderingInfo info) {
332
333 XYItemRendererState state = new StackedXYAreaRendererState(info);
334 // in the rendering process, there is special handling for item
335 // zero, so we can't support processing of visible data items only
336 state.setProcessVisibleItemsOnly(false);
337 return state;
338 }
339
340 /**
341 * Returns the number of passes required by the renderer.
342 *
343 * @return 2.
344 */
345 public int getPassCount() {
346 return 2;
347 }
348
349 /**
350 * Returns the range of values the renderer requires to display all the
351 * items from the specified dataset.
352 *
353 * @param dataset the dataset (<code>null</code> permitted).
354 *
355 * @return The range ([0.0, 0.0] if the dataset contains no values, and
356 * <code>null</code> if the dataset is <code>null</code>).
357 *
358 * @throws ClassCastException if <code>dataset</code> is not an instance
359 * of {@link TableXYDataset}.
360 */
361 public Range findRangeBounds(XYDataset dataset) {
362 if (dataset != null) {
363 return DatasetUtilities.findStackedRangeBounds(
364 (TableXYDataset) dataset);
365 }
366 else {
367 return null;
368 }
369 }
370
371 /**
372 * Draws the visual representation of a single data item.
373 *
374 * @param g2 the graphics device.
375 * @param state the renderer state.
376 * @param dataArea the area within which the data is being drawn.
377 * @param info collects information about the drawing.
378 * @param plot the plot (can be used to obtain standard color information
379 * etc).
380 * @param domainAxis the domain axis.
381 * @param rangeAxis the range axis.
382 * @param dataset the dataset.
383 * @param series the series index (zero-based).
384 * @param item the item index (zero-based).
385 * @param crosshairState information about crosshairs on a plot.
386 * @param pass the pass index.
387 *
388 * @throws ClassCastException if <code>state</code> is not an instance of
389 * <code>StackedXYAreaRendererState</code> or <code>dataset</code>
390 * is not an instance of {@link TableXYDataset}.
391 */
392 public void drawItem(Graphics2D g2,
393 XYItemRendererState state,
394 Rectangle2D dataArea,
395 PlotRenderingInfo info,
396 XYPlot plot,
397 ValueAxis domainAxis,
398 ValueAxis rangeAxis,
399 XYDataset dataset,
400 int series,
401 int item,
402 CrosshairState crosshairState,
403 int pass) {
404
405 PlotOrientation orientation = plot.getOrientation();
406 StackedXYAreaRendererState areaState
407 = (StackedXYAreaRendererState) state;
408 // Get the item count for the series, so that we can know which is the
409 // end of the series.
410 TableXYDataset tdataset = (TableXYDataset) dataset;
411 int itemCount = tdataset.getItemCount();
412
413 // get the data point...
414 double x1 = dataset.getXValue(series, item);
415 double y1 = dataset.getYValue(series, item);
416 boolean nullPoint = false;
417 if (Double.isNaN(y1)) {
418 y1 = 0.0;
419 nullPoint = true;
420 }
421
422 // Get height adjustment based on stack and translate to Java2D values
423 double ph1 = getPreviousHeight(tdataset, series, item);
424 double transX1 = domainAxis.valueToJava2D(x1, dataArea,
425 plot.getDomainAxisEdge());
426 double transY1 = rangeAxis.valueToJava2D(y1 + ph1, dataArea,
427 plot.getRangeAxisEdge());
428
429 // Get series Paint and Stroke
430 Paint seriesPaint = getItemPaint(series, item);
431 Stroke seriesStroke = getItemStroke(series, item);
432
433 if (pass == 0) {
434 // On first pass render the areas, line and outlines
435
436 if (item == 0) {
437 // Create a new Area for the series
438 areaState.setSeriesArea(new Polygon());
439 areaState.setLastSeriesPoints(
440 areaState.getCurrentSeriesPoints());
441 areaState.setCurrentSeriesPoints(new Stack());
442
443 // start from previous height (ph1)
444 double transY2 = rangeAxis.valueToJava2D(ph1, dataArea,
445 plot.getRangeAxisEdge());
446
447 // The first point is (x, 0)
448 if (orientation == PlotOrientation.VERTICAL) {
449 areaState.getSeriesArea().addPoint((int) transX1,
450 (int) transY2);
451 }
452 else if (orientation == PlotOrientation.HORIZONTAL) {
453 areaState.getSeriesArea().addPoint((int) transY2,
454 (int) transX1);
455 }
456 }
457
458 // Add each point to Area (x, y)
459 if (orientation == PlotOrientation.VERTICAL) {
460 Point point = new Point((int) transX1, (int) transY1);
461 areaState.getSeriesArea().addPoint((int) point.getX(),
462 (int) point.getY());
463 areaState.getCurrentSeriesPoints().push(point);
464 }
465 else if (orientation == PlotOrientation.HORIZONTAL) {
466 areaState.getSeriesArea().addPoint((int) transY1,
467 (int) transX1);
468 }
469
470 if (getPlotLines()) {
471 if (item > 0) {
472 // get the previous data point...
473 double x0 = dataset.getXValue(series, item - 1);
474 double y0 = dataset.getYValue(series, item - 1);
475 double ph0 = getPreviousHeight(tdataset, series, item - 1);
476 double transX0 = domainAxis.valueToJava2D(x0, dataArea,
477 plot.getDomainAxisEdge());
478 double transY0 = rangeAxis.valueToJava2D(y0 + ph0,
479 dataArea, plot.getRangeAxisEdge());
480
481 if (orientation == PlotOrientation.VERTICAL) {
482 areaState.getLine().setLine(transX0, transY0, transX1,
483 transY1);
484 }
485 else if (orientation == PlotOrientation.HORIZONTAL) {
486 areaState.getLine().setLine(transY0, transX0, transY1,
487 transX1);
488 }
489 g2.draw(areaState.getLine());
490 }
491 }
492
493 // Check if the item is the last item for the series and number of
494 // items > 0. We can't draw an area for a single point.
495 if (getPlotArea() && item > 0 && item == (itemCount - 1)) {
496
497 double transY2 = rangeAxis.valueToJava2D(ph1, dataArea,
498 plot.getRangeAxisEdge());
499
500 if (orientation == PlotOrientation.VERTICAL) {
501 // Add the last point (x,0)
502 areaState.getSeriesArea().addPoint((int) transX1,
503 (int) transY2);
504 }
505 else if (orientation == PlotOrientation.HORIZONTAL) {
506 // Add the last point (x,0)
507 areaState.getSeriesArea().addPoint((int) transY2,
508 (int) transX1);
509 }
510
511 // Add points from last series to complete the base of the
512 // polygon
513 if (series != 0) {
514 Stack points = areaState.getLastSeriesPoints();
515 while (!points.empty()) {
516 Point point = (Point) points.pop();
517 areaState.getSeriesArea().addPoint((int) point.getX(),
518 (int) point.getY());
519 }
520 }
521
522 // Fill the polygon
523 g2.setPaint(seriesPaint);
524 g2.setStroke(seriesStroke);
525 g2.fill(areaState.getSeriesArea());
526
527 // Draw an outline around the Area.
528 if (isOutline()) {
529 g2.setStroke(lookupSeriesOutlineStroke(series));
530 g2.setPaint(lookupSeriesOutlinePaint(series));
531 g2.draw(areaState.getSeriesArea());
532 }
533 }
534
535 int domainAxisIndex = plot.getDomainAxisIndex(domainAxis);
536 int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis);
537 updateCrosshairValues(crosshairState, x1, ph1 + y1, domainAxisIndex,
538 rangeAxisIndex, transX1, transY1, orientation);
539
540 }
541 else if (pass == 1) {
542 // On second pass render shapes and collect entity and tooltip
543 // information
544
545 Shape shape = null;
546 if (getPlotShapes()) {
547 shape = getItemShape(series, item);
548 if (plot.getOrientation() == PlotOrientation.VERTICAL) {
549 shape = ShapeUtilities.createTranslatedShape(shape,
550 transX1, transY1);
551 }
552 else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
553 shape = ShapeUtilities.createTranslatedShape(shape,
554 transY1, transX1);
555 }
556 if (!nullPoint) {
557 if (getShapePaint() != null) {
558 g2.setPaint(getShapePaint());
559 }
560 else {
561 g2.setPaint(seriesPaint);
562 }
563 if (getShapeStroke() != null) {
564 g2.setStroke(getShapeStroke());
565 }
566 else {
567 g2.setStroke(seriesStroke);
568 }
569 g2.draw(shape);
570 }
571 }
572 else {
573 if (plot.getOrientation() == PlotOrientation.VERTICAL) {
574 shape = new Rectangle2D.Double(transX1 - 3, transY1 - 3,
575 6.0, 6.0);
576 }
577 else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
578 shape = new Rectangle2D.Double(transY1 - 3, transX1 - 3,
579 6.0, 6.0);
580 }
581 }
582
583 // collect entity and tool tip information...
584 if (state.getInfo() != null) {
585 EntityCollection entities = state.getEntityCollection();
586 if (entities != null && shape != null && !nullPoint) {
587 String tip = null;
588 XYToolTipGenerator generator
589 = getToolTipGenerator(series, item);
590 if (generator != null) {
591 tip = generator.generateToolTip(dataset, series, item);
592 }
593 String url = null;
594 if (getURLGenerator() != null) {
595 url = getURLGenerator().generateURL(dataset, series,
596 item);
597 }
598 XYItemEntity entity = new XYItemEntity(shape, dataset,
599 series, item, tip, url);
600 entities.add(entity);
601 }
602 }
603
604 }
605 }
606
607 /**
608 * Calculates the stacked value of the all series up to, but not including
609 * <code>series</code> for the specified item. It returns 0.0 if
610 * <code>series</code> is the first series, i.e. 0.
611 *
612 * @param dataset the dataset.
613 * @param series the series.
614 * @param index the index.
615 *
616 * @return The cumulative value for all series' values up to but excluding
617 * <code>series</code> for <code>index</code>.
618 */
619 protected double getPreviousHeight(TableXYDataset dataset,
620 int series, int index) {
621 double result = 0.0;
622 for (int i = 0; i < series; i++) {
623 double value = dataset.getYValue(i, index);
624 if (!Double.isNaN(value)) {
625 result += value;
626 }
627 }
628 return result;
629 }
630
631 /**
632 * Tests the renderer for equality with an arbitrary object.
633 *
634 * @param obj the object (<code>null</code> permitted).
635 *
636 * @return A boolean.
637 */
638 public boolean equals(Object obj) {
639 if (obj == this) {
640 return true;
641 }
642 if (!(obj instanceof StackedXYAreaRenderer) || !super.equals(obj)) {
643 return false;
644 }
645 StackedXYAreaRenderer that = (StackedXYAreaRenderer) obj;
646 if (!PaintUtilities.equal(this.shapePaint, that.shapePaint)) {
647 return false;
648 }
649 if (!ObjectUtilities.equal(this.shapeStroke, that.shapeStroke)) {
650 return false;
651 }
652 return true;
653 }
654
655 /**
656 * Returns a clone of the renderer.
657 *
658 * @return A clone.
659 *
660 * @throws CloneNotSupportedException if the renderer cannot be cloned.
661 */
662 public Object clone() throws CloneNotSupportedException {
663 return super.clone();
664 }
665
666 /**
667 * Provides serialization support.
668 *
669 * @param stream the input stream.
670 *
671 * @throws IOException if there is an I/O error.
672 * @throws ClassNotFoundException if there is a classpath problem.
673 */
674 private void readObject(ObjectInputStream stream)
675 throws IOException, ClassNotFoundException {
676 stream.defaultReadObject();
677 this.shapePaint = SerialUtilities.readPaint(stream);
678 this.shapeStroke = SerialUtilities.readStroke(stream);
679 }
680
681 /**
682 * Provides serialization support.
683 *
684 * @param stream the output stream.
685 *
686 * @throws IOException if there is an I/O error.
687 */
688 private void writeObject(ObjectOutputStream stream) throws IOException {
689 stream.defaultWriteObject();
690 SerialUtilities.writePaint(this.shapePaint, stream);
691 SerialUtilities.writeStroke(this.shapeStroke, stream);
692 }
693
694 }