001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2009, 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 * XYSeriesCollection.java
029 * -----------------------
030 * (C) Copyright 2001-2009, by Object Refinery Limited and Contributors.
031 *
032 * Original Author: David Gilbert (for Object Refinery Limited);
033 * Contributor(s): Aaron Metzger;
034 *
035 * Changes
036 * -------
037 * 15-Nov-2001 : Version 1 (DG);
038 * 03-Apr-2002 : Added change listener code (DG);
039 * 29-Apr-2002 : Added removeSeries, removeAllSeries methods (ARM);
040 * 07-Oct-2002 : Fixed errors reported by Checkstyle (DG);
041 * 26-Mar-2003 : Implemented Serializable (DG);
042 * 04-Aug-2003 : Added getSeries() method (DG);
043 * 31-Mar-2004 : Modified to use an XYIntervalDelegate.
044 * 05-May-2004 : Now extends AbstractIntervalXYDataset (DG);
045 * 18-Aug-2004 : Moved from org.jfree.data --> org.jfree.data.xy (DG);
046 * 17-Nov-2004 : Updated for changes to DomainInfo interface (DG);
047 * 11-Jan-2005 : Removed deprecated code in preparation for 1.0.0 release (DG);
048 * 28-Mar-2005 : Fixed bug in getSeries(int) method (1170825) (DG);
049 * 05-Oct-2005 : Made the interval delegate a dataset listener (DG);
050 * ------------- JFREECHART 1.0.x ---------------------------------------------
051 * 27-Nov-2006 : Added clone() override (DG);
052 * 08-May-2007 : Added indexOf(XYSeries) method (DG);
053 * 03-Dec-2007 : Added getSeries(Comparable) method (DG);
054 * 22-Apr-2008 : Implemented PublicCloneable (DG);
055 * 27-Feb-2009 : Overridden getDomainOrder() to detect when all series are
056 * sorted in ascending order (DG);
057 * 06-Mar-2009 : Implemented RangeInfo (DG);
058 * 06-Mar-2009 : Fixed equals() implementation (DG);
059 *
060 */
061
062 package org.jfree.data.xy;
063
064 import java.io.Serializable;
065 import java.util.Collections;
066 import java.util.Iterator;
067 import java.util.List;
068
069 import org.jfree.chart.HashUtilities;
070 import org.jfree.data.DomainInfo;
071 import org.jfree.data.DomainOrder;
072 import org.jfree.data.Range;
073 import org.jfree.data.RangeInfo;
074 import org.jfree.data.UnknownKeyException;
075 import org.jfree.data.general.DatasetChangeEvent;
076 import org.jfree.util.ObjectUtilities;
077 import org.jfree.util.PublicCloneable;
078
079 /**
080 * Represents a collection of {@link XYSeries} objects that can be used as a
081 * dataset.
082 */
083 public class XYSeriesCollection extends AbstractIntervalXYDataset
084 implements IntervalXYDataset, DomainInfo, RangeInfo, PublicCloneable,
085 Serializable {
086
087 /** For serialization. */
088 private static final long serialVersionUID = -7590013825931496766L;
089
090 /** The series that are included in the collection. */
091 private List data;
092
093 /** The interval delegate (used to calculate the start and end x-values). */
094 private IntervalXYDelegate intervalDelegate;
095
096 /**
097 * Constructs an empty dataset.
098 */
099 public XYSeriesCollection() {
100 this(null);
101 }
102
103 /**
104 * Constructs a dataset and populates it with a single series.
105 *
106 * @param series the series (<code>null</code> ignored).
107 */
108 public XYSeriesCollection(XYSeries series) {
109 this.data = new java.util.ArrayList();
110 this.intervalDelegate = new IntervalXYDelegate(this, false);
111 addChangeListener(this.intervalDelegate);
112 if (series != null) {
113 this.data.add(series);
114 series.addChangeListener(this);
115 }
116 }
117
118 /**
119 * Returns the order of the domain (X) values, if this is known.
120 *
121 * @return The domain order.
122 */
123 public DomainOrder getDomainOrder() {
124 int seriesCount = getSeriesCount();
125 for (int i = 0; i < seriesCount; i++) {
126 XYSeries s = getSeries(i);
127 if (!s.getAutoSort()) {
128 return DomainOrder.NONE; // we can't be sure of the order
129 }
130 }
131 return DomainOrder.ASCENDING;
132 }
133
134 /**
135 * Adds a series to the collection and sends a {@link DatasetChangeEvent}
136 * to all registered listeners.
137 *
138 * @param series the series (<code>null</code> not permitted).
139 */
140 public void addSeries(XYSeries series) {
141 if (series == null) {
142 throw new IllegalArgumentException("Null 'series' argument.");
143 }
144 this.data.add(series);
145 series.addChangeListener(this);
146 fireDatasetChanged();
147 }
148
149 /**
150 * Removes a series from the collection and sends a
151 * {@link DatasetChangeEvent} to all registered listeners.
152 *
153 * @param series the series index (zero-based).
154 */
155 public void removeSeries(int series) {
156 if ((series < 0) || (series >= getSeriesCount())) {
157 throw new IllegalArgumentException("Series index out of bounds.");
158 }
159
160 // fetch the series, remove the change listener, then remove the series.
161 XYSeries ts = (XYSeries) this.data.get(series);
162 ts.removeChangeListener(this);
163 this.data.remove(series);
164 fireDatasetChanged();
165 }
166
167 /**
168 * Removes a series from the collection and sends a
169 * {@link DatasetChangeEvent} to all registered listeners.
170 *
171 * @param series the series (<code>null</code> not permitted).
172 */
173 public void removeSeries(XYSeries series) {
174 if (series == null) {
175 throw new IllegalArgumentException("Null 'series' argument.");
176 }
177 if (this.data.contains(series)) {
178 series.removeChangeListener(this);
179 this.data.remove(series);
180 fireDatasetChanged();
181 }
182 }
183
184 /**
185 * Removes all the series from the collection and sends a
186 * {@link DatasetChangeEvent} to all registered listeners.
187 */
188 public void removeAllSeries() {
189 // Unregister the collection as a change listener to each series in
190 // the collection.
191 for (int i = 0; i < this.data.size(); i++) {
192 XYSeries series = (XYSeries) this.data.get(i);
193 series.removeChangeListener(this);
194 }
195
196 // Remove all the series from the collection and notify listeners.
197 this.data.clear();
198 fireDatasetChanged();
199 }
200
201 /**
202 * Returns the number of series in the collection.
203 *
204 * @return The series count.
205 */
206 public int getSeriesCount() {
207 return this.data.size();
208 }
209
210 /**
211 * Returns a list of all the series in the collection.
212 *
213 * @return The list (which is unmodifiable).
214 */
215 public List getSeries() {
216 return Collections.unmodifiableList(this.data);
217 }
218
219 /**
220 * Returns the index of the specified series, or -1 if that series is not
221 * present in the dataset.
222 *
223 * @param series the series (<code>null</code> not permitted).
224 *
225 * @return The series index.
226 *
227 * @since 1.0.6
228 */
229 public int indexOf(XYSeries series) {
230 if (series == null) {
231 throw new IllegalArgumentException("Null 'series' argument.");
232 }
233 return this.data.indexOf(series);
234 }
235
236 /**
237 * Returns a series from the collection.
238 *
239 * @param series the series index (zero-based).
240 *
241 * @return The series.
242 *
243 * @throws IllegalArgumentException if <code>series</code> is not in the
244 * range <code>0</code> to <code>getSeriesCount() - 1</code>.
245 */
246 public XYSeries getSeries(int series) {
247 if ((series < 0) || (series >= getSeriesCount())) {
248 throw new IllegalArgumentException("Series index out of bounds");
249 }
250 return (XYSeries) this.data.get(series);
251 }
252
253 /**
254 * Returns a series from the collection.
255 *
256 * @param key the key (<code>null</code> not permitted).
257 *
258 * @return The series with the specified key.
259 *
260 * @throws UnknownKeyException if <code>key</code> is not found in the
261 * collection.
262 *
263 * @since 1.0.9
264 */
265 public XYSeries getSeries(Comparable key) {
266 if (key == null) {
267 throw new IllegalArgumentException("Null 'key' argument.");
268 }
269 Iterator iterator = this.data.iterator();
270 while (iterator.hasNext()) {
271 XYSeries series = (XYSeries) iterator.next();
272 if (key.equals(series.getKey())) {
273 return series;
274 }
275 }
276 throw new UnknownKeyException("Key not found: " + key);
277 }
278
279 /**
280 * Returns the key for a series.
281 *
282 * @param series the series index (in the range <code>0</code> to
283 * <code>getSeriesCount() - 1</code>).
284 *
285 * @return The key for a series.
286 *
287 * @throws IllegalArgumentException if <code>series</code> is not in the
288 * specified range.
289 */
290 public Comparable getSeriesKey(int series) {
291 // defer argument checking
292 return getSeries(series).getKey();
293 }
294
295 /**
296 * Returns the number of items in the specified series.
297 *
298 * @param series the series (zero-based index).
299 *
300 * @return The item count.
301 *
302 * @throws IllegalArgumentException if <code>series</code> is not in the
303 * range <code>0</code> to <code>getSeriesCount() - 1</code>.
304 */
305 public int getItemCount(int series) {
306 // defer argument checking
307 return getSeries(series).getItemCount();
308 }
309
310 /**
311 * Returns the x-value for the specified series and item.
312 *
313 * @param series the series (zero-based index).
314 * @param item the item (zero-based index).
315 *
316 * @return The value.
317 */
318 public Number getX(int series, int item) {
319 XYSeries ts = (XYSeries) this.data.get(series);
320 XYDataItem xyItem = ts.getDataItem(item);
321 return xyItem.getX();
322 }
323
324 /**
325 * Returns the starting X value for the specified series and item.
326 *
327 * @param series the series (zero-based index).
328 * @param item the item (zero-based index).
329 *
330 * @return The starting X value.
331 */
332 public Number getStartX(int series, int item) {
333 return this.intervalDelegate.getStartX(series, item);
334 }
335
336 /**
337 * Returns the ending X value for the specified series and item.
338 *
339 * @param series the series (zero-based index).
340 * @param item the item (zero-based index).
341 *
342 * @return The ending X value.
343 */
344 public Number getEndX(int series, int item) {
345 return this.intervalDelegate.getEndX(series, item);
346 }
347
348 /**
349 * Returns the y-value for the specified series and item.
350 *
351 * @param series the series (zero-based index).
352 * @param index the index of the item of interest (zero-based).
353 *
354 * @return The value (possibly <code>null</code>).
355 */
356 public Number getY(int series, int index) {
357 XYSeries ts = (XYSeries) this.data.get(series);
358 XYDataItem xyItem = ts.getDataItem(index);
359 return xyItem.getY();
360 }
361
362 /**
363 * Returns the starting Y value for the specified series and item.
364 *
365 * @param series the series (zero-based index).
366 * @param item the item (zero-based index).
367 *
368 * @return The starting Y value.
369 */
370 public Number getStartY(int series, int item) {
371 return getY(series, item);
372 }
373
374 /**
375 * Returns the ending Y value for the specified series and item.
376 *
377 * @param series the series (zero-based index).
378 * @param item the item (zero-based index).
379 *
380 * @return The ending Y value.
381 */
382 public Number getEndY(int series, int item) {
383 return getY(series, item);
384 }
385
386 /**
387 * Tests this collection for equality with an arbitrary object.
388 *
389 * @param obj the object (<code>null</code> permitted).
390 *
391 * @return A boolean.
392 */
393 public boolean equals(Object obj) {
394 if (obj == this) {
395 return true;
396 }
397 if (!(obj instanceof XYSeriesCollection)) {
398 return false;
399 }
400 XYSeriesCollection that = (XYSeriesCollection) obj;
401 if (!this.intervalDelegate.equals(that.intervalDelegate)) {
402 return false;
403 }
404 return ObjectUtilities.equal(this.data, that.data);
405 }
406
407 /**
408 * Returns a clone of this instance.
409 *
410 * @return A clone.
411 *
412 * @throws CloneNotSupportedException if there is a problem.
413 */
414 public Object clone() throws CloneNotSupportedException {
415 XYSeriesCollection clone = (XYSeriesCollection) super.clone();
416 clone.data = (List) ObjectUtilities.deepClone(this.data);
417 clone.intervalDelegate
418 = (IntervalXYDelegate) this.intervalDelegate.clone();
419 return clone;
420 }
421
422 /**
423 * Returns a hash code.
424 *
425 * @return A hash code.
426 */
427 public int hashCode() {
428 int hash = 5;
429 hash = HashUtilities.hashCode(hash, this.intervalDelegate);
430 hash = HashUtilities.hashCode(hash, this.data);
431 return hash;
432 }
433
434 /**
435 * Returns the minimum x-value in the dataset.
436 *
437 * @param includeInterval a flag that determines whether or not the
438 * x-interval is taken into account.
439 *
440 * @return The minimum value.
441 */
442 public double getDomainLowerBound(boolean includeInterval) {
443 if (includeInterval) {
444 return this.intervalDelegate.getDomainLowerBound(includeInterval);
445 }
446 else {
447 double result = Double.NaN;
448 int seriesCount = getSeriesCount();
449 for (int s = 0; s < seriesCount; s++) {
450 XYSeries series = getSeries(s);
451 double lowX = series.getMinX();
452 if (Double.isNaN(result)) {
453 result = lowX;
454 }
455 else {
456 if (!Double.isNaN(lowX)) {
457 result = Math.min(result, lowX);
458 }
459 }
460 }
461 return result;
462 }
463 }
464
465 /**
466 * Returns the maximum x-value in the dataset.
467 *
468 * @param includeInterval a flag that determines whether or not the
469 * x-interval is taken into account.
470 *
471 * @return The maximum value.
472 */
473 public double getDomainUpperBound(boolean includeInterval) {
474 if (includeInterval) {
475 return this.intervalDelegate.getDomainUpperBound(includeInterval);
476 }
477 else {
478 double result = Double.NaN;
479 int seriesCount = getSeriesCount();
480 for (int s = 0; s < seriesCount; s++) {
481 XYSeries series = getSeries(s);
482 double hiX = series.getMaxX();
483 if (Double.isNaN(result)) {
484 result = hiX;
485 }
486 else {
487 if (!Double.isNaN(hiX)) {
488 result = Math.max(result, hiX);
489 }
490 }
491 }
492 return result;
493 }
494 }
495
496 /**
497 * Returns the range of the values in this dataset's domain.
498 *
499 * @param includeInterval a flag that determines whether or not the
500 * x-interval is taken into account.
501 *
502 * @return The range (or <code>null</code> if the dataset contains no
503 * values).
504 */
505 public Range getDomainBounds(boolean includeInterval) {
506 if (includeInterval) {
507 return this.intervalDelegate.getDomainBounds(includeInterval);
508 }
509 else {
510 double lower = Double.POSITIVE_INFINITY;
511 double upper = Double.NEGATIVE_INFINITY;
512 int seriesCount = getSeriesCount();
513 for (int s = 0; s < seriesCount; s++) {
514 XYSeries series = getSeries(s);
515 double minX = series.getMinX();
516 if (!Double.isNaN(minX)) {
517 lower = Math.min(lower, minX);
518 }
519 double maxX = series.getMaxX();
520 if (!Double.isNaN(maxX)) {
521 upper = Math.max(upper, maxX);
522 }
523 }
524 if (lower > upper) {
525 return null;
526 }
527 else {
528 return new Range(lower, upper);
529 }
530 }
531 }
532
533 /**
534 * Returns the interval width. This is used to calculate the start and end
535 * x-values, if/when the dataset is used as an {@link IntervalXYDataset}.
536 *
537 * @return The interval width.
538 */
539 public double getIntervalWidth() {
540 return this.intervalDelegate.getIntervalWidth();
541 }
542
543 /**
544 * Sets the interval width and sends a {@link DatasetChangeEvent} to all
545 * registered listeners.
546 *
547 * @param width the width (negative values not permitted).
548 */
549 public void setIntervalWidth(double width) {
550 if (width < 0.0) {
551 throw new IllegalArgumentException("Negative 'width' argument.");
552 }
553 this.intervalDelegate.setFixedIntervalWidth(width);
554 fireDatasetChanged();
555 }
556
557 /**
558 * Returns the interval position factor.
559 *
560 * @return The interval position factor.
561 */
562 public double getIntervalPositionFactor() {
563 return this.intervalDelegate.getIntervalPositionFactor();
564 }
565
566 /**
567 * Sets the interval position factor. This controls where the x-value is in
568 * relation to the interval surrounding the x-value (0.0 means the x-value
569 * will be positioned at the start, 0.5 in the middle, and 1.0 at the end).
570 *
571 * @param factor the factor.
572 */
573 public void setIntervalPositionFactor(double factor) {
574 this.intervalDelegate.setIntervalPositionFactor(factor);
575 fireDatasetChanged();
576 }
577
578 /**
579 * Returns whether the interval width is automatically calculated or not.
580 *
581 * @return Whether the width is automatically calculated or not.
582 */
583 public boolean isAutoWidth() {
584 return this.intervalDelegate.isAutoWidth();
585 }
586
587 /**
588 * Sets the flag that indicates wether the interval width is automatically
589 * calculated or not.
590 *
591 * @param b a boolean.
592 */
593 public void setAutoWidth(boolean b) {
594 this.intervalDelegate.setAutoWidth(b);
595 fireDatasetChanged();
596 }
597
598 /**
599 * Returns the range of the values in this dataset's range.
600 *
601 * @param includeInterval ignored.
602 *
603 * @return The range (or <code>null</code> if the dataset contains no
604 * values).
605 */
606 public Range getRangeBounds(boolean includeInterval) {
607 double lower = Double.POSITIVE_INFINITY;
608 double upper = Double.NEGATIVE_INFINITY;
609 int seriesCount = getSeriesCount();
610 for (int s = 0; s < seriesCount; s++) {
611 XYSeries series = getSeries(s);
612 double minY = series.getMinY();
613 if (!Double.isNaN(minY)) {
614 lower = Math.min(lower, minY);
615 }
616 double maxY = series.getMaxY();
617 if (!Double.isNaN(maxY)) {
618 upper = Math.max(upper, maxY);
619 }
620 }
621 if (lower > upper) {
622 return null;
623 }
624 else {
625 return new Range(lower, upper);
626 }
627 }
628
629 /**
630 * Returns the minimum y-value in the dataset.
631 *
632 * @param includeInterval a flag that determines whether or not the
633 * y-interval is taken into account.
634 *
635 * @return The minimum value.
636 */
637 public double getRangeLowerBound(boolean includeInterval) {
638 double result = Double.NaN;
639 int seriesCount = getSeriesCount();
640 for (int s = 0; s < seriesCount; s++) {
641 XYSeries series = getSeries(s);
642 double lowY = series.getMinY();
643 if (Double.isNaN(result)) {
644 result = lowY;
645 }
646 else {
647 if (!Double.isNaN(lowY)) {
648 result = Math.min(result, lowY);
649 }
650 }
651 }
652 return result;
653 }
654
655 /**
656 * Returns the maximum y-value in the dataset.
657 *
658 * @param includeInterval a flag that determines whether or not the
659 * y-interval is taken into account.
660 *
661 * @return The maximum value.
662 */
663 public double getRangeUpperBound(boolean includeInterval) {
664 double result = Double.NaN;
665 int seriesCount = getSeriesCount();
666 for (int s = 0; s < seriesCount; s++) {
667 XYSeries series = getSeries(s);
668 double hiY = series.getMaxY();
669 if (Double.isNaN(result)) {
670 result = hiY;
671 }
672 else {
673 if (!Double.isNaN(hiY)) {
674 result = Math.max(result, hiY);
675 }
676 }
677 }
678 return result;
679 }
680
681 }