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 * StandardXYItemRenderer.java
029 * ---------------------------
030 * (C) Copyright 2001-2008, by Object Refinery Limited and Contributors.
031 *
032 * Original Author: David Gilbert (for Object Refinery Limited);
033 * Contributor(s): Mark Watson (www.markwatson.com);
034 * Jonathan Nash;
035 * Andreas Schneider;
036 * Norbert Kiesel (for TBD Networks);
037 * Christian W. Zuckschwerdt;
038 * Bill Kelemen;
039 * Nicolas Brodu (for Astrium and EADS Corporate Research
040 * Center);
041 *
042 * Changes:
043 * --------
044 * 19-Oct-2001 : Version 1, based on code by Mark Watson (DG);
045 * 22-Oct-2001 : Renamed DataSource.java --> Dataset.java etc. (DG);
046 * 21-Dec-2001 : Added working line instance to improve performance (DG);
047 * 22-Jan-2002 : Added code to lock crosshairs to data points. Based on code
048 * by Jonathan Nash (DG);
049 * 23-Jan-2002 : Added DrawInfo parameter to drawItem() method (DG);
050 * 28-Mar-2002 : Added a property change listener mechanism so that the
051 * renderer no longer needs to be immutable (DG);
052 * 02-Apr-2002 : Modified to handle null values (DG);
053 * 09-Apr-2002 : Modified draw method to return void. Removed the translated
054 * zero from the drawItem method. Override the initialise()
055 * method to calculate it (DG);
056 * 13-May-2002 : Added code from Andreas Schneider to allow changing
057 * shapes/colors per item (DG);
058 * 24-May-2002 : Incorporated tooltips into chart entities (DG);
059 * 25-Jun-2002 : Removed redundant code (DG);
060 * 05-Aug-2002 : Incorporated URLs for HTML image maps into chart entities (RA);
061 * 08-Aug-2002 : Added discontinuous lines option contributed by
062 * Norbert Kiesel (DG);
063 * 20-Aug-2002 : Added user definable default values to be returned by
064 * protected methods unless overridden by a subclass (DG);
065 * 23-Sep-2002 : Updated for changes in the XYItemRenderer interface (DG);
066 * 02-Oct-2002 : Fixed errors reported by Checkstyle (DG);
067 * 25-Mar-2003 : Implemented Serializable (DG);
068 * 01-May-2003 : Modified drawItem() method signature (DG);
069 * 15-May-2003 : Modified to take into account the plot orientation (DG);
070 * 29-Jul-2003 : Amended code that doesn't compile with JDK 1.2.2 (DG);
071 * 30-Jul-2003 : Modified entity constructor (CZ);
072 * 20-Aug-2003 : Implemented Cloneable and PublicCloneable (DG);
073 * 24-Aug-2003 : Added null/NaN checks in drawItem (BK);
074 * 08-Sep-2003 : Fixed serialization (NB);
075 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
076 * 21-Jan-2004 : Override for getLegendItem() method (DG);
077 * 27-Jan-2004 : Moved working line into state object (DG);
078 * 10-Feb-2004 : Changed drawItem() method to make cut-and-paste overriding
079 * easier (DG);
080 * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState. Renamed
081 * XYToolTipGenerator --> XYItemLabelGenerator (DG);
082 * 08-Jun-2004 : Modified to use getX() and getY() methods (DG);
083 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
084 * getYValue() (DG);
085 * 25-Aug-2004 : Created addEntity() method in superclass (DG);
086 * 08-Oct-2004 : Added 'gapThresholdType' as suggested by Mike Watts (DG);
087 * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG);
088 * 23-Feb-2005 : Fixed getLegendItem() method to show lines. Fixed bug
089 * 1077108 (shape not visible for first item in series) (DG);
090 * 10-Apr-2005 : Fixed item label positioning with horizontal orientation (DG);
091 * 20-Apr-2005 : Use generators for legend tooltips and URLs (DG);
092 * 27-Apr-2005 : Use generator for series label in legend (DG);
093 * ------------- JFREECHART 1.0.x ---------------------------------------------
094 * 15-Jun-2006 : Fixed bug (1380480) for rendering series as path (DG);
095 * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG);
096 * 14-Mar-2007 : Fixed problems with the equals() and clone() methods (DG);
097 * 23-Mar-2007 : Clean-up of shapesFilled attributes (DG);
098 * 20-Apr-2007 : Updated getLegendItem() and drawItem() for renderer
099 * change (DG);
100 * 17-May-2007 : Set datasetIndex and seriesIndex in getLegendItem()
101 * method (DG);
102 * 18-May-2007 : Set dataset and seriesKey for LegendItem (DG);
103 * 08-Jun-2007 : Fixed bug in entity creation (DG);
104 * 21-Nov-2007 : Deprecated override flag methods (DG);
105 * 02-Jun-2008 : Fixed tooltips for data items at lower edges of data area (DG);
106 * 17-Jun-2008 : Apply legend shape, font and paint attributes (DG);
107 *
108 */
109
110 package org.jfree.chart.renderer.xy;
111
112 import java.awt.Graphics2D;
113 import java.awt.Image;
114 import java.awt.Paint;
115 import java.awt.Point;
116 import java.awt.Shape;
117 import java.awt.Stroke;
118 import java.awt.geom.GeneralPath;
119 import java.awt.geom.Line2D;
120 import java.awt.geom.Rectangle2D;
121 import java.io.IOException;
122 import java.io.ObjectInputStream;
123 import java.io.ObjectOutputStream;
124 import java.io.Serializable;
125
126 import org.jfree.chart.LegendItem;
127 import org.jfree.chart.axis.ValueAxis;
128 import org.jfree.chart.entity.EntityCollection;
129 import org.jfree.chart.event.RendererChangeEvent;
130 import org.jfree.chart.labels.XYToolTipGenerator;
131 import org.jfree.chart.plot.CrosshairState;
132 import org.jfree.chart.plot.Plot;
133 import org.jfree.chart.plot.PlotOrientation;
134 import org.jfree.chart.plot.PlotRenderingInfo;
135 import org.jfree.chart.plot.XYPlot;
136 import org.jfree.chart.urls.XYURLGenerator;
137 import org.jfree.data.xy.XYDataset;
138 import org.jfree.io.SerialUtilities;
139 import org.jfree.ui.RectangleEdge;
140 import org.jfree.util.BooleanList;
141 import org.jfree.util.BooleanUtilities;
142 import org.jfree.util.ObjectUtilities;
143 import org.jfree.util.PublicCloneable;
144 import org.jfree.util.ShapeUtilities;
145 import org.jfree.util.UnitType;
146
147 /**
148 * Standard item renderer for an {@link XYPlot}. This class can draw (a)
149 * shapes at each point, or (b) lines between points, or (c) both shapes and
150 * lines.
151 * <P>
152 * This renderer has been retained for historical reasons and, in general, you
153 * should use the {@link XYLineAndShapeRenderer} class instead.
154 */
155 public class StandardXYItemRenderer extends AbstractXYItemRenderer
156 implements XYItemRenderer, Cloneable, PublicCloneable, Serializable {
157
158 /** For serialization. */
159 private static final long serialVersionUID = -3271351259436865995L;
160
161 /** Constant for the type of rendering (shapes only). */
162 public static final int SHAPES = 1;
163
164 /** Constant for the type of rendering (lines only). */
165 public static final int LINES = 2;
166
167 /** Constant for the type of rendering (shapes and lines). */
168 public static final int SHAPES_AND_LINES = SHAPES | LINES;
169
170 /** Constant for the type of rendering (images only). */
171 public static final int IMAGES = 4;
172
173 /** Constant for the type of rendering (discontinuous lines). */
174 public static final int DISCONTINUOUS = 8;
175
176 /** Constant for the type of rendering (discontinuous lines). */
177 public static final int DISCONTINUOUS_LINES = LINES | DISCONTINUOUS;
178
179 /** A flag indicating whether or not shapes are drawn at each XY point. */
180 private boolean baseShapesVisible;
181
182 /** A flag indicating whether or not lines are drawn between XY points. */
183 private boolean plotLines;
184
185 /** A flag indicating whether or not images are drawn between XY points. */
186 private boolean plotImages;
187
188 /** A flag controlling whether or not discontinuous lines are used. */
189 private boolean plotDiscontinuous;
190
191 /** Specifies how the gap threshold value is interpreted. */
192 private UnitType gapThresholdType = UnitType.RELATIVE;
193
194 /** Threshold for deciding when to discontinue a line. */
195 private double gapThreshold = 1.0;
196
197 /**
198 * A flag that controls whether or not shapes are filled for ALL series.
199 *
200 * @deprecated As of 1.0.8, this override should not be used.
201 */
202 private Boolean shapesFilled;
203
204 /**
205 * A table of flags that control (per series) whether or not shapes are
206 * filled.
207 */
208 private BooleanList seriesShapesFilled;
209
210 /** The default value returned by the getShapeFilled() method. */
211 private boolean baseShapesFilled;
212
213 /**
214 * A flag that controls whether or not each series is drawn as a single
215 * path.
216 */
217 private boolean drawSeriesLineAsPath;
218
219 /**
220 * The shape that is used to represent a line in the legend.
221 * This should never be set to <code>null</code>.
222 */
223 private transient Shape legendLine;
224
225 /**
226 * Constructs a new renderer.
227 */
228 public StandardXYItemRenderer() {
229 this(LINES, null);
230 }
231
232 /**
233 * Constructs a new renderer. To specify the type of renderer, use one of
234 * the constants: {@link #SHAPES}, {@link #LINES} or
235 * {@link #SHAPES_AND_LINES}.
236 *
237 * @param type the type.
238 */
239 public StandardXYItemRenderer(int type) {
240 this(type, null);
241 }
242
243 /**
244 * Constructs a new renderer. To specify the type of renderer, use one of
245 * the constants: {@link #SHAPES}, {@link #LINES} or
246 * {@link #SHAPES_AND_LINES}.
247 *
248 * @param type the type of renderer.
249 * @param toolTipGenerator the item label generator (<code>null</code>
250 * permitted).
251 */
252 public StandardXYItemRenderer(int type,
253 XYToolTipGenerator toolTipGenerator) {
254 this(type, toolTipGenerator, null);
255 }
256
257 /**
258 * Constructs a new renderer. To specify the type of renderer, use one of
259 * the constants: {@link #SHAPES}, {@link #LINES} or
260 * {@link #SHAPES_AND_LINES}.
261 *
262 * @param type the type of renderer.
263 * @param toolTipGenerator the item label generator (<code>null</code>
264 * permitted).
265 * @param urlGenerator the URL generator.
266 */
267 public StandardXYItemRenderer(int type,
268 XYToolTipGenerator toolTipGenerator,
269 XYURLGenerator urlGenerator) {
270
271 super();
272 setBaseToolTipGenerator(toolTipGenerator);
273 setURLGenerator(urlGenerator);
274 if ((type & SHAPES) != 0) {
275 this.baseShapesVisible = true;
276 }
277 if ((type & LINES) != 0) {
278 this.plotLines = true;
279 }
280 if ((type & IMAGES) != 0) {
281 this.plotImages = true;
282 }
283 if ((type & DISCONTINUOUS) != 0) {
284 this.plotDiscontinuous = true;
285 }
286
287 this.shapesFilled = null;
288 this.seriesShapesFilled = new BooleanList();
289 this.baseShapesFilled = true;
290 this.legendLine = new Line2D.Double(-7.0, 0.0, 7.0, 0.0);
291 this.drawSeriesLineAsPath = false;
292 }
293
294 /**
295 * Returns true if shapes are being plotted by the renderer.
296 *
297 * @return <code>true</code> if shapes are being plotted by the renderer.
298 *
299 * @see #setBaseShapesVisible
300 */
301 public boolean getBaseShapesVisible() {
302 return this.baseShapesVisible;
303 }
304
305 /**
306 * Sets the flag that controls whether or not a shape is plotted at each
307 * data point.
308 *
309 * @param flag the flag.
310 *
311 * @see #getBaseShapesVisible
312 */
313 public void setBaseShapesVisible(boolean flag) {
314 if (this.baseShapesVisible != flag) {
315 this.baseShapesVisible = flag;
316 fireChangeEvent();
317 }
318 }
319
320 // SHAPES FILLED
321
322 /**
323 * Returns the flag used to control whether or not the shape for an item is
324 * filled.
325 * <p>
326 * The default implementation passes control to the
327 * <code>getSeriesShapesFilled</code> method. You can override this method
328 * if you require different behaviour.
329 *
330 * @param series the series index (zero-based).
331 * @param item the item index (zero-based).
332 *
333 * @return A boolean.
334 *
335 * @see #getSeriesShapesFilled(int)
336 */
337 public boolean getItemShapeFilled(int series, int item) {
338 // return the overall setting, if there is one...
339 if (this.shapesFilled != null) {
340 return this.shapesFilled.booleanValue();
341 }
342
343 // otherwise look up the paint table
344 Boolean flag = this.seriesShapesFilled.getBoolean(series);
345 if (flag != null) {
346 return flag.booleanValue();
347 }
348 else {
349 return this.baseShapesFilled;
350 }
351 }
352
353 /**
354 * Returns the override flag that controls whether or not shapes are filled
355 * for ALL series.
356 *
357 * @return The flag (possibly <code>null</code>).
358 *
359 * @since 1.0.5
360 *
361 * @deprecated As of 1.0.8, you should avoid using this method and rely
362 * on just the per-series ({@link #getSeriesShapesFilled(int)})
363 * and base-level ({@link #getBaseShapesFilled()}) settings.
364 */
365 public Boolean getShapesFilled() {
366 return this.shapesFilled;
367 }
368
369 /**
370 * Sets the override flag that controls whether or not shapes are filled
371 * for ALL series and sends a {@link RendererChangeEvent} to all registered
372 * listeners.
373 *
374 * @param filled the flag.
375 *
376 * @see #setShapesFilled(Boolean)
377 *
378 * @deprecated As of 1.0.8, you should avoid using this method and rely
379 * on just the per-series ({@link #setSeriesShapesFilled(int,
380 * Boolean)}) and base-level ({@link #setBaseShapesVisible(
381 * boolean)}) settings.
382 */
383 public void setShapesFilled(boolean filled) {
384 // here we use BooleanUtilities to remain compatible with JDKs < 1.4
385 setShapesFilled(BooleanUtilities.valueOf(filled));
386 }
387
388 /**
389 * Sets the override flag that controls whether or not shapes are filled
390 * for ALL series and sends a {@link RendererChangeEvent} to all registered
391 * listeners.
392 *
393 * @param filled the flag (<code>null</code> permitted).
394 *
395 * @see #setShapesFilled(boolean)
396 *
397 * @deprecated As of 1.0.8, you should avoid using this method and rely
398 * on just the per-series ({@link #setSeriesShapesFilled(int,
399 * Boolean)}) and base-level ({@link #setBaseShapesVisible(
400 * boolean)}) settings.
401 */
402 public void setShapesFilled(Boolean filled) {
403 this.shapesFilled = filled;
404 fireChangeEvent();
405 }
406
407 /**
408 * Returns the flag used to control whether or not the shapes for a series
409 * are filled.
410 *
411 * @param series the series index (zero-based).
412 *
413 * @return A boolean.
414 */
415 public Boolean getSeriesShapesFilled(int series) {
416 return this.seriesShapesFilled.getBoolean(series);
417 }
418
419 /**
420 * Sets the 'shapes filled' flag for a series and sends a
421 * {@link RendererChangeEvent} to all registered listeners.
422 *
423 * @param series the series index (zero-based).
424 * @param flag the flag.
425 *
426 * @see #getSeriesShapesFilled(int)
427 */
428 public void setSeriesShapesFilled(int series, Boolean flag) {
429 this.seriesShapesFilled.setBoolean(series, flag);
430 fireChangeEvent();
431 }
432
433 /**
434 * Returns the base 'shape filled' attribute.
435 *
436 * @return The base flag.
437 *
438 * @see #setBaseShapesFilled(boolean)
439 */
440 public boolean getBaseShapesFilled() {
441 return this.baseShapesFilled;
442 }
443
444 /**
445 * Sets the base 'shapes filled' flag and sends a
446 * {@link RendererChangeEvent} to all registered listeners.
447 *
448 * @param flag the flag.
449 *
450 * @see #getBaseShapesFilled()
451 */
452 public void setBaseShapesFilled(boolean flag) {
453 this.baseShapesFilled = flag;
454 }
455
456 /**
457 * Returns true if lines are being plotted by the renderer.
458 *
459 * @return <code>true</code> if lines are being plotted by the renderer.
460 *
461 * @see #setPlotLines(boolean)
462 */
463 public boolean getPlotLines() {
464 return this.plotLines;
465 }
466
467 /**
468 * Sets the flag that controls whether or not a line is plotted between
469 * each data point and sends a {@link RendererChangeEvent} to all
470 * registered listeners.
471 *
472 * @param flag the flag.
473 *
474 * @see #getPlotLines()
475 */
476 public void setPlotLines(boolean flag) {
477 if (this.plotLines != flag) {
478 this.plotLines = flag;
479 fireChangeEvent();
480 }
481 }
482
483 /**
484 * Returns the gap threshold type (relative or absolute).
485 *
486 * @return The type.
487 *
488 * @see #setGapThresholdType(UnitType)
489 */
490 public UnitType getGapThresholdType() {
491 return this.gapThresholdType;
492 }
493
494 /**
495 * Sets the gap threshold type and sends a {@link RendererChangeEvent} to
496 * all registered listeners.
497 *
498 * @param thresholdType the type (<code>null</code> not permitted).
499 *
500 * @see #getGapThresholdType()
501 */
502 public void setGapThresholdType(UnitType thresholdType) {
503 if (thresholdType == null) {
504 throw new IllegalArgumentException(
505 "Null 'thresholdType' argument.");
506 }
507 this.gapThresholdType = thresholdType;
508 fireChangeEvent();
509 }
510
511 /**
512 * Returns the gap threshold for discontinuous lines.
513 *
514 * @return The gap threshold.
515 *
516 * @see #setGapThreshold(double)
517 */
518 public double getGapThreshold() {
519 return this.gapThreshold;
520 }
521
522 /**
523 * Sets the gap threshold for discontinuous lines and sends a
524 * {@link RendererChangeEvent} to all registered listeners.
525 *
526 * @param t the threshold.
527 *
528 * @see #getGapThreshold()
529 */
530 public void setGapThreshold(double t) {
531 this.gapThreshold = t;
532 fireChangeEvent();
533 }
534
535 /**
536 * Returns true if images are being plotted by the renderer.
537 *
538 * @return <code>true</code> if images are being plotted by the renderer.
539 *
540 * @see #setPlotImages(boolean)
541 */
542 public boolean getPlotImages() {
543 return this.plotImages;
544 }
545
546 /**
547 * Sets the flag that controls whether or not an image is drawn at each
548 * data point and sends a {@link RendererChangeEvent} to all registered
549 * listeners.
550 *
551 * @param flag the flag.
552 *
553 * @see #getPlotImages()
554 */
555 public void setPlotImages(boolean flag) {
556 if (this.plotImages != flag) {
557 this.plotImages = flag;
558 fireChangeEvent();
559 }
560 }
561
562 /**
563 * Returns a flag that controls whether or not the renderer shows
564 * discontinuous lines.
565 *
566 * @return <code>true</code> if lines should be discontinuous.
567 */
568 public boolean getPlotDiscontinuous() {
569 return this.plotDiscontinuous;
570 }
571
572 /**
573 * Sets the flag that controls whether or not the renderer shows
574 * discontinuous lines, and sends a {@link RendererChangeEvent} to all
575 * registered listeners.
576 *
577 * @param flag the new flag value.
578 *
579 * @since 1.0.5
580 */
581 public void setPlotDiscontinuous(boolean flag) {
582 if (this.plotDiscontinuous != flag) {
583 this.plotDiscontinuous = flag;
584 fireChangeEvent();
585 }
586 }
587
588 /**
589 * Returns a flag that controls whether or not each series is drawn as a
590 * single path.
591 *
592 * @return A boolean.
593 *
594 * @see #setDrawSeriesLineAsPath(boolean)
595 */
596 public boolean getDrawSeriesLineAsPath() {
597 return this.drawSeriesLineAsPath;
598 }
599
600 /**
601 * Sets the flag that controls whether or not each series is drawn as a
602 * single path.
603 *
604 * @param flag the flag.
605 *
606 * @see #getDrawSeriesLineAsPath()
607 */
608 public void setDrawSeriesLineAsPath(boolean flag) {
609 this.drawSeriesLineAsPath = flag;
610 }
611
612 /**
613 * Returns the shape used to represent a line in the legend.
614 *
615 * @return The legend line (never <code>null</code>).
616 *
617 * @see #setLegendLine(Shape)
618 */
619 public Shape getLegendLine() {
620 return this.legendLine;
621 }
622
623 /**
624 * Sets the shape used as a line in each legend item and sends a
625 * {@link RendererChangeEvent} to all registered listeners.
626 *
627 * @param line the line (<code>null</code> not permitted).
628 *
629 * @see #getLegendLine()
630 */
631 public void setLegendLine(Shape line) {
632 if (line == null) {
633 throw new IllegalArgumentException("Null 'line' argument.");
634 }
635 this.legendLine = line;
636 fireChangeEvent();
637 }
638
639 /**
640 * Returns a legend item for a series.
641 *
642 * @param datasetIndex the dataset index (zero-based).
643 * @param series the series index (zero-based).
644 *
645 * @return A legend item for the series.
646 */
647 public LegendItem getLegendItem(int datasetIndex, int series) {
648 XYPlot plot = getPlot();
649 if (plot == null) {
650 return null;
651 }
652 LegendItem result = null;
653 XYDataset dataset = plot.getDataset(datasetIndex);
654 if (dataset != null) {
655 if (getItemVisible(series, 0)) {
656 String label = getLegendItemLabelGenerator().generateLabel(
657 dataset, series);
658 String description = label;
659 String toolTipText = null;
660 if (getLegendItemToolTipGenerator() != null) {
661 toolTipText = getLegendItemToolTipGenerator().generateLabel(
662 dataset, series);
663 }
664 String urlText = null;
665 if (getLegendItemURLGenerator() != null) {
666 urlText = getLegendItemURLGenerator().generateLabel(
667 dataset, series);
668 }
669 Shape shape = lookupLegendShape(series);
670 boolean shapeFilled = getItemShapeFilled(series, 0);
671 Paint paint = lookupSeriesPaint(series);
672 Paint linePaint = paint;
673 Stroke lineStroke = lookupSeriesStroke(series);
674 result = new LegendItem(label, description, toolTipText,
675 urlText, this.baseShapesVisible, shape, shapeFilled,
676 paint, !shapeFilled, paint, lineStroke,
677 this.plotLines, this.legendLine, lineStroke, linePaint);
678 result.setLabelFont(lookupLegendTextFont(series));
679 Paint labelPaint = lookupLegendTextPaint(series);
680 if (labelPaint != null) {
681 result.setLabelPaint(labelPaint);
682 }
683 result.setDataset(dataset);
684 result.setDatasetIndex(datasetIndex);
685 result.setSeriesKey(dataset.getSeriesKey(series));
686 result.setSeriesIndex(series);
687 }
688 }
689 return result;
690 }
691
692 /**
693 * Records the state for the renderer. This is used to preserve state
694 * information between calls to the drawItem() method for a single chart
695 * drawing.
696 */
697 public static class State extends XYItemRendererState {
698
699 /** The path for the current series. */
700 public GeneralPath seriesPath;
701
702 /** The series index. */
703 private int seriesIndex;
704
705 /**
706 * A flag that indicates if the last (x, y) point was 'good'
707 * (non-null).
708 */
709 private boolean lastPointGood;
710
711 /**
712 * Creates a new state instance.
713 *
714 * @param info the plot rendering info.
715 */
716 public State(PlotRenderingInfo info) {
717 super(info);
718 }
719
720 /**
721 * Returns a flag that indicates if the last point drawn (in the
722 * current series) was 'good' (non-null).
723 *
724 * @return A boolean.
725 */
726 public boolean isLastPointGood() {
727 return this.lastPointGood;
728 }
729
730 /**
731 * Sets a flag that indicates if the last point drawn (in the current
732 * series) was 'good' (non-null).
733 *
734 * @param good the flag.
735 */
736 public void setLastPointGood(boolean good) {
737 this.lastPointGood = good;
738 }
739
740 /**
741 * Returns the series index for the current path.
742 *
743 * @return The series index for the current path.
744 */
745 public int getSeriesIndex() {
746 return this.seriesIndex;
747 }
748
749 /**
750 * Sets the series index for the current path.
751 *
752 * @param index the index.
753 */
754 public void setSeriesIndex(int index) {
755 this.seriesIndex = index;
756 }
757 }
758
759 /**
760 * Initialises the renderer.
761 * <P>
762 * This method will be called before the first item is rendered, giving the
763 * renderer an opportunity to initialise any state information it wants to
764 * maintain. The renderer can do nothing if it chooses.
765 *
766 * @param g2 the graphics device.
767 * @param dataArea the area inside the axes.
768 * @param plot the plot.
769 * @param data the data.
770 * @param info an optional info collection object to return data back to
771 * the caller.
772 *
773 * @return The renderer state.
774 */
775 public XYItemRendererState initialise(Graphics2D g2,
776 Rectangle2D dataArea,
777 XYPlot plot,
778 XYDataset data,
779 PlotRenderingInfo info) {
780
781 State state = new State(info);
782 state.seriesPath = new GeneralPath();
783 state.seriesIndex = -1;
784 return state;
785
786 }
787
788 /**
789 * Draws the visual representation of a single data item.
790 *
791 * @param g2 the graphics device.
792 * @param state the renderer state.
793 * @param dataArea the area within which the data is being drawn.
794 * @param info collects information about the drawing.
795 * @param plot the plot (can be used to obtain standard color information
796 * etc).
797 * @param domainAxis the domain axis.
798 * @param rangeAxis the range axis.
799 * @param dataset the dataset.
800 * @param series the series index (zero-based).
801 * @param item the item index (zero-based).
802 * @param crosshairState crosshair information for the plot
803 * (<code>null</code> permitted).
804 * @param pass the pass index.
805 */
806 public void drawItem(Graphics2D g2,
807 XYItemRendererState state,
808 Rectangle2D dataArea,
809 PlotRenderingInfo info,
810 XYPlot plot,
811 ValueAxis domainAxis,
812 ValueAxis rangeAxis,
813 XYDataset dataset,
814 int series,
815 int item,
816 CrosshairState crosshairState,
817 int pass) {
818
819 boolean itemVisible = getItemVisible(series, item);
820
821 // setup for collecting optional entity info...
822 Shape entityArea = null;
823 EntityCollection entities = null;
824 if (info != null) {
825 entities = info.getOwner().getEntityCollection();
826 }
827
828 PlotOrientation orientation = plot.getOrientation();
829 Paint paint = getItemPaint(series, item);
830 Stroke seriesStroke = getItemStroke(series, item);
831 g2.setPaint(paint);
832 g2.setStroke(seriesStroke);
833
834 // get the data point...
835 double x1 = dataset.getXValue(series, item);
836 double y1 = dataset.getYValue(series, item);
837 if (Double.isNaN(x1) || Double.isNaN(y1)) {
838 itemVisible = false;
839 }
840
841 RectangleEdge xAxisLocation = plot.getDomainAxisEdge();
842 RectangleEdge yAxisLocation = plot.getRangeAxisEdge();
843 double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation);
844 double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation);
845
846 if (getPlotLines()) {
847 if (this.drawSeriesLineAsPath) {
848 State s = (State) state;
849 if (s.getSeriesIndex() != series) {
850 // we are starting a new series path
851 s.seriesPath.reset();
852 s.lastPointGood = false;
853 s.setSeriesIndex(series);
854 }
855
856 // update path to reflect latest point
857 if (itemVisible && !Double.isNaN(transX1)
858 && !Double.isNaN(transY1)) {
859 float x = (float) transX1;
860 float y = (float) transY1;
861 if (orientation == PlotOrientation.HORIZONTAL) {
862 x = (float) transY1;
863 y = (float) transX1;
864 }
865 if (s.isLastPointGood()) {
866 // TODO: check threshold
867 s.seriesPath.lineTo(x, y);
868 }
869 else {
870 s.seriesPath.moveTo(x, y);
871 }
872 s.setLastPointGood(true);
873 }
874 else {
875 s.setLastPointGood(false);
876 }
877 if (item == dataset.getItemCount(series) - 1) {
878 if (s.seriesIndex == series) {
879 // draw path
880 g2.setStroke(lookupSeriesStroke(series));
881 g2.setPaint(lookupSeriesPaint(series));
882 g2.draw(s.seriesPath);
883 }
884 }
885 }
886
887 else if (item != 0 && itemVisible) {
888 // get the previous data point...
889 double x0 = dataset.getXValue(series, item - 1);
890 double y0 = dataset.getYValue(series, item - 1);
891 if (!Double.isNaN(x0) && !Double.isNaN(y0)) {
892 boolean drawLine = true;
893 if (getPlotDiscontinuous()) {
894 // only draw a line if the gap between the current and
895 // previous data point is within the threshold
896 int numX = dataset.getItemCount(series);
897 double minX = dataset.getXValue(series, 0);
898 double maxX = dataset.getXValue(series, numX - 1);
899 if (this.gapThresholdType == UnitType.ABSOLUTE) {
900 drawLine = Math.abs(x1 - x0) <= this.gapThreshold;
901 }
902 else {
903 drawLine = Math.abs(x1 - x0) <= ((maxX - minX)
904 / numX * getGapThreshold());
905 }
906 }
907 if (drawLine) {
908 double transX0 = domainAxis.valueToJava2D(x0, dataArea,
909 xAxisLocation);
910 double transY0 = rangeAxis.valueToJava2D(y0, dataArea,
911 yAxisLocation);
912
913 // only draw if we have good values
914 if (Double.isNaN(transX0) || Double.isNaN(transY0)
915 || Double.isNaN(transX1) || Double.isNaN(transY1)) {
916 return;
917 }
918
919 if (orientation == PlotOrientation.HORIZONTAL) {
920 state.workingLine.setLine(transY0, transX0,
921 transY1, transX1);
922 }
923 else if (orientation == PlotOrientation.VERTICAL) {
924 state.workingLine.setLine(transX0, transY0,
925 transX1, transY1);
926 }
927
928 if (state.workingLine.intersects(dataArea)) {
929 g2.draw(state.workingLine);
930 }
931 }
932 }
933 }
934 }
935
936 // we needed to get this far even for invisible items, to ensure that
937 // seriesPath updates happened, but now there is nothing more we need
938 // to do for non-visible items...
939 if (!itemVisible) {
940 return;
941 }
942
943 if (getBaseShapesVisible()) {
944
945 Shape shape = getItemShape(series, item);
946 if (orientation == PlotOrientation.HORIZONTAL) {
947 shape = ShapeUtilities.createTranslatedShape(shape, transY1,
948 transX1);
949 }
950 else if (orientation == PlotOrientation.VERTICAL) {
951 shape = ShapeUtilities.createTranslatedShape(shape, transX1,
952 transY1);
953 }
954 if (shape.intersects(dataArea)) {
955 if (getItemShapeFilled(series, item)) {
956 g2.fill(shape);
957 }
958 else {
959 g2.draw(shape);
960 }
961 }
962 entityArea = shape;
963
964 }
965
966 if (getPlotImages()) {
967 Image image = getImage(plot, series, item, transX1, transY1);
968 if (image != null) {
969 Point hotspot = getImageHotspot(plot, series, item, transX1,
970 transY1, image);
971 g2.drawImage(image, (int) (transX1 - hotspot.getX()),
972 (int) (transY1 - hotspot.getY()), null);
973 entityArea = new Rectangle2D.Double(transX1 - hotspot.getX(),
974 transY1 - hotspot.getY(), image.getWidth(null),
975 image.getHeight(null));
976 }
977
978 }
979
980 double xx = transX1;
981 double yy = transY1;
982 if (orientation == PlotOrientation.HORIZONTAL) {
983 xx = transY1;
984 yy = transX1;
985 }
986
987 // draw the item label if there is one...
988 if (isItemLabelVisible(series, item)) {
989 drawItemLabel(g2, orientation, dataset, series, item, xx, yy,
990 (y1 < 0.0));
991 }
992
993 int domainAxisIndex = plot.getDomainAxisIndex(domainAxis);
994 int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis);
995 updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex,
996 rangeAxisIndex, transX1, transY1, orientation);
997
998 // add an entity for the item...
999 if (entities != null && isPointInRect(dataArea, xx, yy)) {
1000 addEntity(entities, entityArea, dataset, series, item, xx, yy);
1001 }
1002
1003 }
1004
1005 /**
1006 * Tests this renderer for equality with another object.
1007 *
1008 * @param obj the object (<code>null</code> permitted).
1009 *
1010 * @return A boolean.
1011 */
1012 public boolean equals(Object obj) {
1013
1014 if (obj == this) {
1015 return true;
1016 }
1017 if (!(obj instanceof StandardXYItemRenderer)) {
1018 return false;
1019 }
1020 StandardXYItemRenderer that = (StandardXYItemRenderer) obj;
1021 if (this.baseShapesVisible != that.baseShapesVisible) {
1022 return false;
1023 }
1024 if (this.plotLines != that.plotLines) {
1025 return false;
1026 }
1027 if (this.plotImages != that.plotImages) {
1028 return false;
1029 }
1030 if (this.plotDiscontinuous != that.plotDiscontinuous) {
1031 return false;
1032 }
1033 if (this.gapThresholdType != that.gapThresholdType) {
1034 return false;
1035 }
1036 if (this.gapThreshold != that.gapThreshold) {
1037 return false;
1038 }
1039 if (!ObjectUtilities.equal(this.shapesFilled, that.shapesFilled)) {
1040 return false;
1041 }
1042 if (!this.seriesShapesFilled.equals(that.seriesShapesFilled)) {
1043 return false;
1044 }
1045 if (this.baseShapesFilled != that.baseShapesFilled) {
1046 return false;
1047 }
1048 if (this.drawSeriesLineAsPath != that.drawSeriesLineAsPath) {
1049 return false;
1050 }
1051 if (!ShapeUtilities.equal(this.legendLine, that.legendLine)) {
1052 return false;
1053 }
1054 return super.equals(obj);
1055
1056 }
1057
1058 /**
1059 * Returns a clone of the renderer.
1060 *
1061 * @return A clone.
1062 *
1063 * @throws CloneNotSupportedException if the renderer cannot be cloned.
1064 */
1065 public Object clone() throws CloneNotSupportedException {
1066 StandardXYItemRenderer clone = (StandardXYItemRenderer) super.clone();
1067 clone.seriesShapesFilled
1068 = (BooleanList) this.seriesShapesFilled.clone();
1069 clone.legendLine = ShapeUtilities.clone(this.legendLine);
1070 return clone;
1071 }
1072
1073 ////////////////////////////////////////////////////////////////////////////
1074 // PROTECTED METHODS
1075 // These provide the opportunity to subclass the standard renderer and
1076 // create custom effects.
1077 ////////////////////////////////////////////////////////////////////////////
1078
1079 /**
1080 * Returns the image used to draw a single data item.
1081 *
1082 * @param plot the plot (can be used to obtain standard color information
1083 * etc).
1084 * @param series the series index.
1085 * @param item the item index.
1086 * @param x the x value of the item.
1087 * @param y the y value of the item.
1088 *
1089 * @return The image.
1090 *
1091 * @see #getPlotImages()
1092 */
1093 protected Image getImage(Plot plot, int series, int item,
1094 double x, double y) {
1095 // this method must be overridden if you want to display images
1096 return null;
1097 }
1098
1099 /**
1100 * Returns the hotspot of the image used to draw a single data item.
1101 * The hotspot is the point relative to the top left of the image
1102 * that should indicate the data item. The default is the center of the
1103 * image.
1104 *
1105 * @param plot the plot (can be used to obtain standard color information
1106 * etc).
1107 * @param image the image (can be used to get size information about the
1108 * image)
1109 * @param series the series index
1110 * @param item the item index
1111 * @param x the x value of the item
1112 * @param y the y value of the item
1113 *
1114 * @return The hotspot used to draw the data item.
1115 */
1116 protected Point getImageHotspot(Plot plot, int series, int item,
1117 double x, double y, Image image) {
1118
1119 int height = image.getHeight(null);
1120 int width = image.getWidth(null);
1121 return new Point(width / 2, height / 2);
1122
1123 }
1124
1125 /**
1126 * Provides serialization support.
1127 *
1128 * @param stream the input stream.
1129 *
1130 * @throws IOException if there is an I/O error.
1131 * @throws ClassNotFoundException if there is a classpath problem.
1132 */
1133 private void readObject(ObjectInputStream stream)
1134 throws IOException, ClassNotFoundException {
1135 stream.defaultReadObject();
1136 this.legendLine = SerialUtilities.readShape(stream);
1137 }
1138
1139 /**
1140 * Provides serialization support.
1141 *
1142 * @param stream the output stream.
1143 *
1144 * @throws IOException if there is an I/O error.
1145 */
1146 private void writeObject(ObjectOutputStream stream) throws IOException {
1147 stream.defaultWriteObject();
1148 SerialUtilities.writeShape(this.legendLine, stream);
1149 }
1150
1151 }