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