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 * XYStepAreaRenderer.java
029 * -----------------------
030 * (C) Copyright 2003-2007, by Matthias Rose and Contributors.
031 *
032 * Original Author: Matthias Rose (based on XYAreaRenderer.java);
033 * Contributor(s): David Gilbert (for Object Refinery Limited);
034 *
035 * $Id: XYStepAreaRenderer.java,v 1.7.2.7 2007/05/04 11:12:16 mungady Exp $
036 *
037 * Changes:
038 * --------
039 * 07-Oct-2003 : Version 1, contributed by Matthias Rose (DG);
040 * 10-Feb-2004 : Added some getter and setter methods (DG);
041 * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState. Renamed
042 * XYToolTipGenerator --> XYItemLabelGenerator (DG);
043 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
044 * getYValue() (DG);
045 * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG);
046 * 06-Jul-2005 : Renamed get/setPlotShapes() --> get/setShapesVisible() (DG);
047 * ------------- JFREECHART 1.0.x ---------------------------------------------
048 * 06-Jul-2006 : Modified to call dataset methods that return double
049 * primitives only (DG);
050 * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG);
051 * 14-Feb-2007 : Added equals() method override (DG);
052 * 04-May-2007 : Set processVisibleItemsOnly flag to false (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.Polygon;
061 import java.awt.Shape;
062 import java.awt.Stroke;
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.entity.XYItemEntity;
069 import org.jfree.chart.event.RendererChangeEvent;
070 import org.jfree.chart.labels.XYToolTipGenerator;
071 import org.jfree.chart.plot.CrosshairState;
072 import org.jfree.chart.plot.PlotOrientation;
073 import org.jfree.chart.plot.PlotRenderingInfo;
074 import org.jfree.chart.plot.XYPlot;
075 import org.jfree.chart.urls.XYURLGenerator;
076 import org.jfree.data.xy.XYDataset;
077 import org.jfree.util.PublicCloneable;
078 import org.jfree.util.ShapeUtilities;
079
080 /**
081 * A step chart renderer that fills the area between the step and the x-axis.
082 */
083 public class XYStepAreaRenderer extends AbstractXYItemRenderer
084 implements XYItemRenderer,
085 Cloneable,
086 PublicCloneable,
087 Serializable {
088
089 /** For serialization. */
090 private static final long serialVersionUID = -7311560779702649635L;
091
092 /** Useful constant for specifying the type of rendering (shapes only). */
093 public static final int SHAPES = 1;
094
095 /** Useful constant for specifying the type of rendering (area only). */
096 public static final int AREA = 2;
097
098 /**
099 * Useful constant for specifying the type of rendering (area and shapes).
100 */
101 public static final int AREA_AND_SHAPES = 3;
102
103 /** A flag indicating whether or not shapes are drawn at each XY point. */
104 private boolean shapesVisible;
105
106 /** A flag that controls whether or not shapes are filled for ALL series. */
107 private boolean shapesFilled;
108
109 /** A flag indicating whether or not Area are drawn at each XY point. */
110 private boolean plotArea;
111
112 /** A flag that controls whether or not the outline is shown. */
113 private boolean showOutline;
114
115 /** Area of the complete series */
116 protected transient Polygon pArea = null;
117
118 /**
119 * The value on the range axis which defines the 'lower' border of the
120 * area.
121 */
122 private double rangeBase;
123
124 /**
125 * Constructs a new renderer.
126 */
127 public XYStepAreaRenderer() {
128 this(AREA);
129 }
130
131 /**
132 * Constructs a new renderer.
133 *
134 * @param type the type of the renderer.
135 */
136 public XYStepAreaRenderer(int type) {
137 this(type, null, null);
138 }
139
140 /**
141 * Constructs a new renderer.
142 * <p>
143 * To specify the type of renderer, use one of the constants:
144 * AREA, SHAPES or AREA_AND_SHAPES.
145 *
146 * @param type the type of renderer.
147 * @param toolTipGenerator the tool tip generator to use
148 * (<code>null</code> permitted).
149 * @param urlGenerator the URL generator (<code>null</code> permitted).
150 */
151 public XYStepAreaRenderer(int type,
152 XYToolTipGenerator toolTipGenerator,
153 XYURLGenerator urlGenerator) {
154
155 super();
156 setBaseToolTipGenerator(toolTipGenerator);
157 setURLGenerator(urlGenerator);
158
159 if (type == AREA) {
160 this.plotArea = true;
161 }
162 else if (type == SHAPES) {
163 this.shapesVisible = true;
164 }
165 else if (type == AREA_AND_SHAPES) {
166 this.plotArea = true;
167 this.shapesVisible = true;
168 }
169 this.showOutline = false;
170 }
171
172 /**
173 * Returns a flag that controls whether or not outlines of the areas are
174 * drawn.
175 *
176 * @return The flag.
177 *
178 * @see #setOutline(boolean)
179 */
180 public boolean isOutline() {
181 return this.showOutline;
182 }
183
184 /**
185 * Sets a flag that controls whether or not outlines of the areas are
186 * drawn, and sends a {@link RendererChangeEvent} to all registered
187 * listeners.
188 *
189 * @param show the flag.
190 *
191 * @see #isOutline()
192 */
193 public void setOutline(boolean show) {
194 this.showOutline = show;
195 notifyListeners(new RendererChangeEvent(this));
196 }
197
198 /**
199 * Returns true if shapes are being plotted by the renderer.
200 *
201 * @return <code>true</code> if shapes are being plotted by the renderer.
202 *
203 * @see #setShapesVisible(boolean)
204 */
205 public boolean getShapesVisible() {
206 return this.shapesVisible;
207 }
208
209 /**
210 * Sets the flag that controls whether or not shapes are displayed for each
211 * data item, and sends a {@link RendererChangeEvent} to all registered
212 * listeners.
213 *
214 * @param flag the flag.
215 *
216 * @see #getShapesVisible()
217 */
218 public void setShapesVisible(boolean flag) {
219 this.shapesVisible = flag;
220 notifyListeners(new RendererChangeEvent(this));
221 }
222
223 /**
224 * Returns the flag that controls whether or not the shapes are filled.
225 *
226 * @return A boolean.
227 *
228 * @see #setShapesFilled(boolean)
229 */
230 public boolean isShapesFilled() {
231 return this.shapesFilled;
232 }
233
234 /**
235 * Sets the 'shapes filled' for ALL series.
236 *
237 * @param filled the flag.
238 *
239 * @see #isShapesFilled()
240 */
241 public void setShapesFilled(boolean filled) {
242 this.shapesFilled = filled;
243 notifyListeners(new RendererChangeEvent(this));
244 }
245
246 /**
247 * Returns true if Area is being plotted by the renderer.
248 *
249 * @return <code>true</code> if Area is being plotted by the renderer.
250 *
251 * @see #setPlotArea(boolean)
252 */
253 public boolean getPlotArea() {
254 return this.plotArea;
255 }
256
257 /**
258 * Sets a flag that controls whether or not areas are drawn for each data
259 * item.
260 *
261 * @param flag the flag.
262 *
263 * @see #getPlotArea()
264 */
265 public void setPlotArea(boolean flag) {
266 this.plotArea = flag;
267 notifyListeners(new RendererChangeEvent(this));
268 }
269
270 /**
271 * Returns the value on the range axis which defines the 'lower' border of
272 * the area.
273 *
274 * @return <code>double</code> the value on the range axis which defines
275 * the 'lower' border of the area.
276 *
277 * @see #setRangeBase(double)
278 */
279 public double getRangeBase() {
280 return this.rangeBase;
281 }
282
283 /**
284 * Sets the value on the range axis which defines the default border of the
285 * area. E.g. setRangeBase(Double.NEGATIVE_INFINITY) lets areas always
286 * reach the lower border of the plotArea.
287 *
288 * @param val the value on the range axis which defines the default border
289 * of the area.
290 *
291 * @see #getRangeBase()
292 */
293 public void setRangeBase(double val) {
294 this.rangeBase = val;
295 notifyListeners(new RendererChangeEvent(this));
296 }
297
298 /**
299 * Initialises the renderer. Here we calculate the Java2D y-coordinate for
300 * zero, since all the bars have their bases fixed at zero.
301 *
302 * @param g2 the graphics device.
303 * @param dataArea the area inside the axes.
304 * @param plot the plot.
305 * @param data the data.
306 * @param info an optional info collection object to return data back to
307 * the caller.
308 *
309 * @return The number of passes required by the renderer.
310 */
311 public XYItemRendererState initialise(Graphics2D g2,
312 Rectangle2D dataArea,
313 XYPlot plot,
314 XYDataset data,
315 PlotRenderingInfo info) {
316
317
318 XYItemRendererState state = super.initialise(g2, dataArea, plot, data,
319 info);
320 // disable visible items optimisation - it doesn't work for this
321 // renderer...
322 state.setProcessVisibleItemsOnly(false);
323 return state;
324
325 }
326
327
328 /**
329 * Draws the visual representation of a single data item.
330 *
331 * @param g2 the graphics device.
332 * @param state the renderer state.
333 * @param dataArea the area within which the data is being drawn.
334 * @param info collects information about the drawing.
335 * @param plot the plot (can be used to obtain standard color information
336 * etc).
337 * @param domainAxis the domain axis.
338 * @param rangeAxis the range axis.
339 * @param dataset the dataset.
340 * @param series the series index (zero-based).
341 * @param item the item index (zero-based).
342 * @param crosshairState crosshair information for the plot
343 * (<code>null</code> permitted).
344 * @param pass the pass index.
345 */
346 public void drawItem(Graphics2D g2,
347 XYItemRendererState state,
348 Rectangle2D dataArea,
349 PlotRenderingInfo info,
350 XYPlot plot,
351 ValueAxis domainAxis,
352 ValueAxis rangeAxis,
353 XYDataset dataset,
354 int series,
355 int item,
356 CrosshairState crosshairState,
357 int pass) {
358
359 PlotOrientation orientation = plot.getOrientation();
360
361 // Get the item count for the series, so that we can know which is the
362 // end of the series.
363 int itemCount = dataset.getItemCount(series);
364
365 Paint paint = getItemPaint(series, item);
366 Stroke seriesStroke = getItemStroke(series, item);
367 g2.setPaint(paint);
368 g2.setStroke(seriesStroke);
369
370 // get the data point...
371 double x1 = dataset.getXValue(series, item);
372 double y1 = dataset.getYValue(series, item);
373 double x = x1;
374 double y = Double.isNaN(y1) ? getRangeBase() : y1;
375 double transX1 = domainAxis.valueToJava2D(x, dataArea,
376 plot.getDomainAxisEdge());
377 double transY1 = rangeAxis.valueToJava2D(y, dataArea,
378 plot.getRangeAxisEdge());
379
380 // avoid possible sun.dc.pr.PRException: endPath: bad path
381 transY1 = restrictValueToDataArea(transY1, plot, dataArea);
382
383 if (this.pArea == null && !Double.isNaN(y1)) {
384
385 // Create a new Area for the series
386 this.pArea = new Polygon();
387
388 // start from Y = rangeBase
389 double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea,
390 plot.getRangeAxisEdge());
391
392 // avoid possible sun.dc.pr.PRException: endPath: bad path
393 transY2 = restrictValueToDataArea(transY2, plot, dataArea);
394
395 // The first point is (x, this.baseYValue)
396 if (orientation == PlotOrientation.VERTICAL) {
397 this.pArea.addPoint((int) transX1, (int) transY2);
398 }
399 else if (orientation == PlotOrientation.HORIZONTAL) {
400 this.pArea.addPoint((int) transY2, (int) transX1);
401 }
402 }
403
404 double transX0 = 0;
405 double transY0 = restrictValueToDataArea(getRangeBase(), plot,
406 dataArea);
407
408 double x0;
409 double y0;
410 if (item > 0) {
411 // get the previous data point...
412 x0 = dataset.getXValue(series, item - 1);
413 y0 = Double.isNaN(y1) ? y1 : dataset.getYValue(series, item - 1);
414
415 x = x0;
416 y = Double.isNaN(y0) ? getRangeBase() : y0;
417 transX0 = domainAxis.valueToJava2D(x, dataArea,
418 plot.getDomainAxisEdge());
419 transY0 = rangeAxis.valueToJava2D(y, dataArea,
420 plot.getRangeAxisEdge());
421
422 // avoid possible sun.dc.pr.PRException: endPath: bad path
423 transY0 = restrictValueToDataArea(transY0, plot, dataArea);
424
425 if (Double.isNaN(y1)) {
426 // NULL value -> insert point on base line
427 // instead of 'step point'
428 transX1 = transX0;
429 transY0 = transY1;
430 }
431 if (transY0 != transY1) {
432 // not just a horizontal bar but need to perform a 'step'.
433 if (orientation == PlotOrientation.VERTICAL) {
434 this.pArea.addPoint((int) transX1, (int) transY0);
435 }
436 else if (orientation == PlotOrientation.HORIZONTAL) {
437 this.pArea.addPoint((int) transY0, (int) transX1);
438 }
439 }
440 }
441
442 Shape shape = null;
443 if (!Double.isNaN(y1)) {
444 // Add each point to Area (x, y)
445 if (orientation == PlotOrientation.VERTICAL) {
446 this.pArea.addPoint((int) transX1, (int) transY1);
447 }
448 else if (orientation == PlotOrientation.HORIZONTAL) {
449 this.pArea.addPoint((int) transY1, (int) transX1);
450 }
451
452 if (getShapesVisible()) {
453 shape = getItemShape(series, item);
454 if (orientation == PlotOrientation.VERTICAL) {
455 shape = ShapeUtilities.createTranslatedShape(shape,
456 transX1, transY1);
457 }
458 else if (orientation == PlotOrientation.HORIZONTAL) {
459 shape = ShapeUtilities.createTranslatedShape(shape,
460 transY1, transX1);
461 }
462 if (isShapesFilled()) {
463 g2.fill(shape);
464 }
465 else {
466 g2.draw(shape);
467 }
468 }
469 else {
470 if (orientation == PlotOrientation.VERTICAL) {
471 shape = new Rectangle2D.Double(transX1 - 2, transY1 - 2,
472 4.0, 4.0);
473 }
474 else if (orientation == PlotOrientation.HORIZONTAL) {
475 shape = new Rectangle2D.Double(transY1 - 2, transX1 - 2,
476 4.0, 4.0);
477 }
478 }
479 }
480
481 // Check if the item is the last item for the series or if it
482 // is a NULL value and number of items > 0. We can't draw an area for
483 // a single point.
484 if (getPlotArea() && item > 0 && this.pArea != null
485 && (item == (itemCount - 1) || Double.isNaN(y1))) {
486
487 double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea,
488 plot.getRangeAxisEdge());
489
490 // avoid possible sun.dc.pr.PRException: endPath: bad path
491 transY2 = restrictValueToDataArea(transY2, plot, dataArea);
492
493 if (orientation == PlotOrientation.VERTICAL) {
494 // Add the last point (x,0)
495 this.pArea.addPoint((int) transX1, (int) transY2);
496 }
497 else if (orientation == PlotOrientation.HORIZONTAL) {
498 // Add the last point (x,0)
499 this.pArea.addPoint((int) transY2, (int) transX1);
500 }
501
502 // fill the polygon
503 g2.fill(this.pArea);
504
505 // draw an outline around the Area.
506 if (isOutline()) {
507 g2.setStroke(plot.getOutlineStroke());
508 g2.setPaint(plot.getOutlinePaint());
509 g2.draw(this.pArea);
510 }
511
512 // start new area when needed (see above)
513 this.pArea = null;
514 }
515
516 // do we need to update the crosshair values?
517 if (!Double.isNaN(y1)) {
518 int domainAxisIndex = plot.getDomainAxisIndex(domainAxis);
519 int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis);
520 updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex,
521 rangeAxisIndex, transX1, transY1, orientation);
522 }
523
524 // collect entity and tool tip information...
525 if (state.getInfo() != null) {
526 EntityCollection entities = state.getEntityCollection();
527 if (entities != null && shape != null) {
528 String tip = null;
529 XYToolTipGenerator generator
530 = getToolTipGenerator(series, item);
531 if (generator != null) {
532 tip = generator.generateToolTip(dataset, series, item);
533 }
534 String url = null;
535 if (getURLGenerator() != null) {
536 url = getURLGenerator().generateURL(dataset, series, item);
537 }
538 XYItemEntity entity = new XYItemEntity(shape, dataset, series,
539 item, tip, url);
540 entities.add(entity);
541 }
542 }
543 }
544
545 /**
546 * Tests this renderer for equality with an arbitrary object.
547 *
548 * @param obj the object (<code>null</code> permitted).
549 *
550 * @return A boolean.
551 */
552 public boolean equals(Object obj) {
553 if (obj == this) {
554 return true;
555 }
556 if (!(obj instanceof XYStepAreaRenderer)) {
557 return false;
558 }
559 XYStepAreaRenderer that = (XYStepAreaRenderer) obj;
560 if (this.showOutline != that.showOutline) {
561 return false;
562 }
563 if (this.shapesVisible != that.shapesVisible) {
564 return false;
565 }
566 if (this.shapesFilled != that.shapesFilled) {
567 return false;
568 }
569 if (this.plotArea != that.plotArea) {
570 return false;
571 }
572 if (this.rangeBase != that.rangeBase) {
573 return false;
574 }
575 return super.equals(obj);
576 }
577
578 /**
579 * Returns a clone of the renderer.
580 *
581 * @return A clone.
582 *
583 * @throws CloneNotSupportedException if the renderer cannot be cloned.
584 */
585 public Object clone() throws CloneNotSupportedException {
586 return super.clone();
587 }
588
589 /**
590 * Helper method which returns a value if it lies
591 * inside the visible dataArea and otherwise the corresponding
592 * coordinate on the border of the dataArea. The PlotOrientation
593 * is taken into account.
594 * Useful to avoid possible sun.dc.pr.PRException: endPath: bad path
595 * which occurs when trying to draw lines/shapes which in large part
596 * lie outside of the visible dataArea.
597 *
598 * @param value the value which shall be
599 * @param dataArea the area within which the data is being drawn.
600 * @param plot the plot (can be used to obtain standard color
601 * information etc).
602 * @return <code>double</code> value inside the data area.
603 */
604 protected static double restrictValueToDataArea(double value,
605 XYPlot plot,
606 Rectangle2D dataArea) {
607 double min = 0;
608 double max = 0;
609 if (plot.getOrientation() == PlotOrientation.VERTICAL) {
610 min = dataArea.getMinY();
611 max = dataArea.getMaxY();
612 }
613 else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
614 min = dataArea.getMinX();
615 max = dataArea.getMaxX();
616 }
617 if (value < min) {
618 value = min;
619 }
620 else if (value > max) {
621 value = max;
622 }
623 return value;
624 }
625
626 }