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 * XYShapeRenderer.java
029 * --------------------
030 * (C) Copyright 2008, by Andreas Haumer, xS+S and Contributors.
031 *
032 * Original Author: Martin Hoeller (x Software + Systeme xS+S - Andreas
033 * Haumer);
034 * Contributor(s): David Gilbert (for Object Refinery Limited);
035 *
036 * Changes:
037 * --------
038 * 17-Sep-2008 : Version 1, based on a contribution from Martin Hoeller with
039 * amendments by David Gilbert (DG);
040 *
041 */
042
043 package org.jfree.chart.renderer.xy;
044
045 import java.awt.BasicStroke;
046 import java.awt.Color;
047 import java.awt.Graphics2D;
048 import java.awt.Paint;
049 import java.awt.Shape;
050 import java.awt.Stroke;
051 import java.awt.geom.Ellipse2D;
052 import java.awt.geom.Line2D;
053 import java.awt.geom.Rectangle2D;
054 import java.io.IOException;
055 import java.io.ObjectInputStream;
056 import java.io.ObjectOutputStream;
057 import java.io.Serializable;
058
059 import org.jfree.chart.axis.ValueAxis;
060 import org.jfree.chart.entity.EntityCollection;
061 import org.jfree.chart.event.RendererChangeEvent;
062 import org.jfree.chart.plot.CrosshairState;
063 import org.jfree.chart.plot.PlotOrientation;
064 import org.jfree.chart.plot.PlotRenderingInfo;
065 import org.jfree.chart.plot.XYPlot;
066 import org.jfree.chart.renderer.LookupPaintScale;
067 import org.jfree.chart.renderer.PaintScale;
068 import org.jfree.data.Range;
069 import org.jfree.data.general.DatasetUtilities;
070 import org.jfree.data.xy.XYDataset;
071 import org.jfree.data.xy.XYZDataset;
072 import org.jfree.io.SerialUtilities;
073 import org.jfree.util.PublicCloneable;
074 import org.jfree.util.ShapeUtilities;
075
076 /**
077 * A renderer that draws shapes at (x, y) coordinates and, if the dataset
078 * is an instance of {@link XYZDataset}, fills the shapes with a paint that
079 * is based on the z-value (the paint is obtained from a lookup table). The
080 * renderer also allows for optional guidelines, horizontal and vertical lines
081 * connecting the shape to the edges of the plot.
082 * <br><br>
083 * The example shown here is generated by the
084 * <code>XYShapeRendererDemo1.java</code> program included in the JFreeChart
085 * demo collection:
086 * <br><br>
087 * <img src="../../../../../images/XYShapeRendererSample.png"
088 * alt="XYShapeRendererSample.png" />
089 * <br><br>
090 * This renderer has similarities to, but also differences from, the
091 * {@link XYLineAndShapeRenderer}.
092 *
093 * @since 1.0.11
094 */
095 public class XYShapeRenderer extends AbstractXYItemRenderer
096 implements XYItemRenderer, Cloneable, Serializable {
097
098 /** Auto generated serial version id. */
099 private static final long serialVersionUID = 8320552104211173221L;
100
101 /** The paint scale. */
102 private PaintScale paintScale;
103
104 /** A flag that controls whether or not the shape outlines are drawn. */
105 private boolean drawOutlines;
106
107 /**
108 * A flag that controls whether or not the outline paint is used (if not,
109 * the regular paint is used).
110 */
111 private boolean useOutlinePaint;
112
113 /**
114 * A flag that controls whether or not the fill paint is used (if not,
115 * the fill paint is used).
116 */
117 private boolean useFillPaint;
118
119 /** Flag indicating if guide lines should be drawn for every item. */
120 private boolean guideLinesVisible;
121
122 /** The paint used for drawing the guide lines. */
123 private transient Paint guideLinePaint;
124
125 /** The stroke used for drawing the guide lines. */
126 private transient Stroke guideLineStroke;
127
128 /**
129 * Creates a new <code>XYShapeRenderer</code> instance with default
130 * attributes.
131 */
132 public XYShapeRenderer() {
133 this.paintScale = new LookupPaintScale();
134 this.useFillPaint = false;
135 this.drawOutlines = false;
136 this.useOutlinePaint = true;
137 this.guideLinesVisible = false;
138 this.guideLinePaint = Color.darkGray;
139 this.guideLineStroke = new BasicStroke();
140 setBaseShape(new Ellipse2D.Double(-5.0, -5.0, 10.0, 10.0));
141 setAutoPopulateSeriesShape(false);
142 }
143
144 /**
145 * Returns the paint scale used by the renderer.
146 *
147 * @return The paint scale (never <code>null</code>).
148 *
149 * @see #setPaintScale(PaintScale)
150 */
151 public PaintScale getPaintScale() {
152 return this.paintScale;
153 }
154
155 /**
156 * Sets the paint scale used by the renderer and sends a
157 * {@link RendererChangeEvent} to all registered listeners.
158 *
159 * @param scale the scale (<code>null</code> not permitted).
160 *
161 * @see #getPaintScale()
162 */
163 public void setPaintScale(PaintScale scale) {
164 if (scale == null) {
165 throw new IllegalArgumentException("Null 'scale' argument.");
166 }
167 this.paintScale = scale;
168 notifyListeners(new RendererChangeEvent(this));
169 }
170
171 /**
172 * Returns <code>true</code> if outlines should be drawn for shapes, and
173 * <code>false</code> otherwise.
174 *
175 * @return A boolean.
176 *
177 * @see #setDrawOutlines(boolean)
178 */
179 public boolean getDrawOutlines() {
180 return this.drawOutlines;
181 }
182
183 /**
184 * Sets the flag that controls whether outlines are drawn for
185 * shapes, and sends a {@link RendererChangeEvent} to all registered
186 * listeners.
187 * <P>
188 * In some cases, shapes look better if they do NOT have an outline, but
189 * this flag allows you to set your own preference.
190 *
191 * @param flag the flag.
192 *
193 * @see #getDrawOutlines()
194 */
195 public void setDrawOutlines(boolean flag) {
196 this.drawOutlines = flag;
197 fireChangeEvent();
198 }
199
200 /**
201 * Returns <code>true</code> if the renderer should use the fill paint
202 * setting to fill shapes, and <code>false</code> if it should just
203 * use the regular paint.
204 * <p>
205 * Refer to <code>XYLineAndShapeRendererDemo2.java</code> to see the
206 * effect of this flag.
207 *
208 * @return A boolean.
209 *
210 * @see #setUseFillPaint(boolean)
211 * @see #getUseOutlinePaint()
212 */
213 public boolean getUseFillPaint() {
214 return this.useFillPaint;
215 }
216
217 /**
218 * Sets the flag that controls whether the fill paint is used to fill
219 * shapes, and sends a {@link RendererChangeEvent} to all
220 * registered listeners.
221 *
222 * @param flag the flag.
223 *
224 * @see #getUseFillPaint()
225 */
226 public void setUseFillPaint(boolean flag) {
227 this.useFillPaint = flag;
228 fireChangeEvent();
229 }
230
231 /**
232 * Returns the flag that controls whether the outline paint is used for
233 * shape outlines. If not, the regular series paint is used.
234 *
235 * @return A boolean.
236 *
237 * @see #setUseOutlinePaint(boolean)
238 */
239 public boolean getUseOutlinePaint() {
240 return this.useOutlinePaint;
241 }
242
243 /**
244 * Sets the flag that controls whether the outline paint is used for shape
245 * outlines, and sends a {@link RendererChangeEvent} to all registered
246 * listeners.
247 *
248 * @param use the flag.
249 *
250 * @see #getUseOutlinePaint()
251 */
252 public void setUseOutlinePaint(boolean use) {
253 this.useOutlinePaint = use;
254 fireChangeEvent();
255 }
256
257 /**
258 * Returns a flag that controls whether or not guide lines are drawn for
259 * each data item (the lines are horizontal and vertical "crosshairs"
260 * linking the data point to the axes).
261 *
262 * @return A boolean.
263 *
264 * @see #setGuideLinesVisible(boolean)
265 */
266 public boolean isGuideLinesVisible() {
267 return this.guideLinesVisible;
268 }
269
270 /**
271 * Sets the flag that controls whether or not guide lines are drawn for
272 * each data item and sends a {@link RendererChangeEvent} to all registered
273 * listeners.
274 *
275 * @param visible the new flag value.
276 *
277 * @see #isGuideLinesVisible()
278 */
279 public void setGuideLinesVisible(boolean visible) {
280 this.guideLinesVisible = visible;
281 fireChangeEvent();
282 }
283
284 /**
285 * Returns the paint used to draw the guide lines.
286 *
287 * @return The paint (never <code>null</code>).
288 *
289 * @see #setGuideLinePaint(Paint)
290 */
291 public Paint getGuideLinePaint() {
292 return this.guideLinePaint;
293 }
294
295 /**
296 * Sets the paint used to draw the guide lines and sends a
297 * {@link RendererChangeEvent} to all registered listeners.
298 *
299 * @param paint the paint (<code>null</code> not permitted).
300 *
301 * @see #getGuideLinePaint()
302 */
303 public void setGuideLinePaint(Paint paint) {
304 if (paint == null) {
305 throw new IllegalArgumentException("Null 'paint' argument.");
306 }
307 this.guideLinePaint = paint;
308 fireChangeEvent();
309 }
310
311 /**
312 * Returns the stroke used to draw the guide lines.
313 *
314 * @return The stroke.
315 *
316 * @see #setGuideLineStroke(Stroke)
317 */
318 public Stroke getGuideLineStroke() {
319 return this.guideLineStroke;
320 }
321
322 /**
323 * Sets the stroke used to draw the guide lines and sends a
324 * {@link RendererChangeEvent} to all registered listeners.
325 *
326 * @param stroke the stroke (<code>null</code> not permitted).
327 *
328 * @see #getGuideLineStroke()
329 */
330 public void setGuideLineStroke(Stroke stroke) {
331 if (stroke == null) {
332 throw new IllegalArgumentException("Null 'stroke' argument.");
333 }
334 this.guideLineStroke = stroke;
335 fireChangeEvent();
336 }
337
338 /**
339 * Returns the lower and upper bounds (range) of the x-values in the
340 * specified dataset.
341 *
342 * @param dataset the dataset (<code>null</code> permitted).
343 *
344 * @return The range (<code>null</code> if the dataset is <code>null</code>
345 * or empty).
346 */
347 public Range findDomainBounds(XYDataset dataset) {
348 if (dataset != null) {
349 Range r = DatasetUtilities.findDomainBounds(dataset, false);
350 double offset = 0; // TODO getSeriesShape(n).getBounds().width / 2;
351 return new Range(r.getLowerBound() + offset,
352 r.getUpperBound() + offset);
353 }
354 else {
355 return null;
356 }
357 }
358
359 /**
360 * Returns the range of values the renderer requires to display all the
361 * items from the specified dataset.
362 *
363 * @param dataset the dataset (<code>null</code> permitted).
364 *
365 * @return The range (<code>null</code> if the dataset is <code>null</code>
366 * or empty).
367 */
368 public Range findRangeBounds(XYDataset dataset) {
369 if (dataset != null) {
370 Range r = DatasetUtilities.findRangeBounds(dataset, false);
371 double offset = 0; // TODO getSeriesShape(n).getBounds().height / 2;
372 return new Range(r.getLowerBound() + offset, r.getUpperBound()
373 + offset);
374 }
375 else {
376 return null;
377 }
378 }
379
380 /**
381 * Returns the number of passes required by this renderer.
382 *
383 * @return <code>2</code>.
384 */
385 public int getPassCount() {
386 return 2;
387 }
388
389 /**
390 * Draws the block representing the specified item.
391 *
392 * @param g2 the graphics device.
393 * @param state the state.
394 * @param dataArea the data area.
395 * @param info the plot rendering info.
396 * @param plot the plot.
397 * @param domainAxis the x-axis.
398 * @param rangeAxis the y-axis.
399 * @param dataset the dataset.
400 * @param series the series index.
401 * @param item the item index.
402 * @param crosshairState the crosshair state.
403 * @param pass the pass index.
404 */
405 public void drawItem(Graphics2D g2, XYItemRendererState state,
406 Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
407 ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset,
408 int series, int item, CrosshairState crosshairState, int pass) {
409
410 Shape hotspot = null;
411 EntityCollection entities = null;
412 if (info != null) {
413 entities = info.getOwner().getEntityCollection();
414 }
415
416 double x = dataset.getXValue(series, item);
417 double y = dataset.getYValue(series, item);
418 if (Double.isNaN(x) || Double.isNaN(y)) {
419 // can't draw anything
420 return;
421 }
422
423 double transX = domainAxis.valueToJava2D(x, dataArea,
424 plot.getDomainAxisEdge());
425 double transY = rangeAxis.valueToJava2D(y, dataArea,
426 plot.getRangeAxisEdge());
427
428 PlotOrientation orientation = plot.getOrientation();
429
430 // draw optional guide lines
431 if ((pass == 0) && this.guideLinesVisible) {
432 g2.setStroke(this.guideLineStroke);
433 g2.setPaint(this.guideLinePaint);
434 if (orientation == PlotOrientation.HORIZONTAL) {
435 g2.draw(new Line2D.Double(transY, dataArea.getMinY(), transY,
436 dataArea.getMaxY()));
437 g2.draw(new Line2D.Double(dataArea.getMinX(), transX,
438 dataArea.getMaxX(), transX));
439 }
440 else {
441 g2.draw(new Line2D.Double(transX, dataArea.getMinY(), transX,
442 dataArea.getMaxY()));
443 g2.draw(new Line2D.Double(dataArea.getMinX(), transY,
444 dataArea.getMaxX(), transY));
445 }
446 }
447 else if (pass == 1) {
448 Shape shape = getItemShape(series, item);
449 if (orientation == PlotOrientation.HORIZONTAL) {
450 shape = ShapeUtilities.createTranslatedShape(shape, transY,
451 transX);
452 }
453 else if (orientation == PlotOrientation.VERTICAL) {
454 shape = ShapeUtilities.createTranslatedShape(shape, transX,
455 transY);
456 }
457 hotspot = shape;
458 if (shape.intersects(dataArea)) {
459 //if (getItemShapeFilled(series, item)) {
460 g2.setPaint(getPaint(dataset, series, item));
461 g2.fill(shape);
462 //}
463 if (this.drawOutlines) {
464 if (getUseOutlinePaint()) {
465 g2.setPaint(getItemOutlinePaint(series, item));
466 }
467 else {
468 g2.setPaint(getItemPaint(series, item));
469 }
470 g2.setStroke(getItemOutlineStroke(series, item));
471 g2.draw(shape);
472 }
473 }
474
475 // add an entity for the item...
476 if (entities != null) {
477 addEntity(entities, hotspot, dataset, series, item, transX,
478 transY);
479 }
480 }
481 }
482
483 /**
484 * Get the paint for a given series and item from a dataset.
485 *
486 * @param dataset the dataset..
487 * @param series the series index.
488 * @param item the item index.
489 *
490 * @return The paint.
491 */
492 protected Paint getPaint(XYDataset dataset, int series, int item) {
493 Paint p = null;
494 if (dataset instanceof XYZDataset) {
495 double z = ((XYZDataset) dataset).getZValue(series, item);
496 p = this.paintScale.getPaint(z);
497 }
498 else {
499 if (this.useFillPaint) {
500 p = getItemFillPaint(series, item);
501 }
502 else {
503 p = getItemPaint(series, item);
504 }
505 }
506 return p;
507 }
508
509 /**
510 * Tests this instance for equality with an arbitrary object. This method
511 * returns <code>true</code> if and only if:
512 * <ul>
513 * <li><code>obj</code> is an instance of <code>XYShapeRenderer</code> (not
514 * <code>null</code>);</li>
515 * <li><code>obj</code> has the same field values as this
516 * <code>XYShapeRenderer</code>;</li>
517 * </ul>
518 *
519 * @param obj the object (<code>null</code> permitted).
520 *
521 * @return A boolean.
522 */
523 public boolean equals(Object obj) {
524 if (obj == this) {
525 return true;
526 }
527 if (!(obj instanceof XYShapeRenderer)) {
528 return false;
529 }
530 XYShapeRenderer that = (XYShapeRenderer) obj;
531 if ((this.paintScale == null && that.paintScale != null)
532 || (!this.paintScale.equals(that.paintScale))) {
533 return false;
534 }
535 if (this.drawOutlines != that.drawOutlines) {
536 return false;
537 }
538 if (this.useOutlinePaint != that.useOutlinePaint) {
539 return false;
540 }
541 if (this.useFillPaint != that.useFillPaint) {
542 return false;
543 }
544 if (this.guideLinesVisible != that.guideLinesVisible) {
545 return false;
546 }
547 if ((this.guideLinePaint == null && that.guideLinePaint != null)
548 || (!this.guideLinePaint.equals(that.guideLinePaint)))
549 return false;
550 if ((this.guideLineStroke == null && that.guideLineStroke != null)
551 || (!this.guideLineStroke.equals(that.guideLineStroke)))
552 return false;
553
554 return super.equals(obj);
555 }
556
557 /**
558 * Returns a clone of this renderer.
559 *
560 * @return A clone of this renderer.
561 *
562 * @throws CloneNotSupportedException if there is a problem creating the
563 * clone.
564 */
565 public Object clone() throws CloneNotSupportedException {
566 XYShapeRenderer clone = (XYShapeRenderer) super.clone();
567 if (this.paintScale instanceof PublicCloneable) {
568 PublicCloneable pc = (PublicCloneable) this.paintScale;
569 clone.paintScale = (PaintScale) pc.clone();
570 }
571 return clone;
572 }
573
574 /**
575 * Provides serialization support.
576 *
577 * @param stream the input stream.
578 *
579 * @throws IOException if there is an I/O error.
580 * @throws ClassNotFoundException if there is a classpath problem.
581 */
582 private void readObject(ObjectInputStream stream)
583 throws IOException, ClassNotFoundException {
584 stream.defaultReadObject();
585 this.guideLinePaint = SerialUtilities.readPaint(stream);
586 this.guideLineStroke = SerialUtilities.readStroke(stream);
587 }
588
589 /**
590 * Provides serialization support.
591 *
592 * @param stream the output stream.
593 *
594 * @throws IOException if there is an I/O error.
595 */
596 private void writeObject(ObjectOutputStream stream) throws IOException {
597 stream.defaultWriteObject();
598 SerialUtilities.writePaint(this.guideLinePaint, stream);
599 SerialUtilities.writeStroke(this.guideLineStroke, stream);
600 }
601
602 }