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