001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2007, 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 * DefaultTableXYDataset.java
029 * --------------------------
030 * (C) Copyright 2003-2007, by Richard Atkinson and Contributors.
031 *
032 * Original Author: Richard Atkinson;
033 * Contributor(s): Jody Brownell;
034 * David Gilbert (for Object Refinery Limited);
035 * Andreas Schroeder;
036 *
037 * Changes:
038 * --------
039 * 27-Jul-2003 : XYDataset that forces each series to have a value for every
040 * X-point which is essential for stacked XY area charts (RA);
041 * 18-Aug-2003 : Fixed event notification when removing and updating
042 * series (RA);
043 * 22-Sep-2003 : Functionality moved from TableXYDataset to
044 * DefaultTableXYDataset (RA);
045 * 23-Dec-2003 : Added patch for large datasets, submitted by Jody
046 * Brownell (DG);
047 * 16-Feb-2004 : Added pruning methods (DG);
048 * 31-Mar-2004 : Provisional implementation of IntervalXYDataset (AS);
049 * 01-Apr-2004 : Sound implementation of IntervalXYDataset (AS);
050 * 05-May-2004 : Now extends AbstractIntervalXYDataset (DG);
051 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
052 * getYValue() (DG);
053 * 18-Aug-2004 : Moved from org.jfree.data --> org.jfree.data.xy (DG);
054 * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0
055 * release (DG);
056 * 05-Oct-2005 : Made the interval delegate a dataset listener (DG);
057 * 02-Feb-2007 : Removed author tags all over JFreeChart sources (DG);
058 *
059 */
060
061 package org.jfree.data.xy;
062
063 import java.util.ArrayList;
064 import java.util.HashSet;
065 import java.util.Iterator;
066 import java.util.List;
067
068 import org.jfree.data.DomainInfo;
069 import org.jfree.data.Range;
070 import org.jfree.data.general.DatasetChangeEvent;
071 import org.jfree.data.general.DatasetUtilities;
072 import org.jfree.data.general.SeriesChangeEvent;
073 import org.jfree.util.ObjectUtilities;
074
075 /**
076 * An {@link XYDataset} where every series shares the same x-values (required
077 * for generating stacked area charts).
078 */
079 public class DefaultTableXYDataset extends AbstractIntervalXYDataset
080 implements TableXYDataset,
081 IntervalXYDataset, DomainInfo {
082
083 /**
084 * Storage for the data - this list will contain zero, one or many
085 * XYSeries objects.
086 */
087 private List data = null;
088
089 /** Storage for the x values. */
090 private HashSet xPoints = null;
091
092 /** A flag that controls whether or not events are propogated. */
093 private boolean propagateEvents = true;
094
095 /** A flag that controls auto pruning. */
096 private boolean autoPrune = false;
097
098 /** The delegate used to control the interval width. */
099 private IntervalXYDelegate intervalDelegate;
100
101 /**
102 * Creates a new empty dataset.
103 */
104 public DefaultTableXYDataset() {
105 this(false);
106 }
107
108 /**
109 * Creates a new empty dataset.
110 *
111 * @param autoPrune a flag that controls whether or not x-values are
112 * removed whenever the corresponding y-values are all
113 * <code>null</code>.
114 */
115 public DefaultTableXYDataset(boolean autoPrune) {
116 this.autoPrune = autoPrune;
117 this.data = new ArrayList();
118 this.xPoints = new HashSet();
119 this.intervalDelegate = new IntervalXYDelegate(this, false);
120 addChangeListener(this.intervalDelegate);
121 }
122
123 /**
124 * Returns the flag that controls whether or not x-values are removed from
125 * the dataset when the corresponding y-values are all <code>null</code>.
126 *
127 * @return A boolean.
128 */
129 public boolean isAutoPrune() {
130 return this.autoPrune;
131 }
132
133 /**
134 * Adds a series to the collection and sends a {@link DatasetChangeEvent}
135 * to all registered listeners. The series should be configured to NOT
136 * allow duplicate x-values.
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 if (series.getAllowDuplicateXValues()) {
145 throw new IllegalArgumentException(
146 "Cannot accept XYSeries that allow duplicate values. "
147 + "Use XYSeries(seriesName, <sort>, false) constructor."
148 );
149 }
150 updateXPoints(series);
151 this.data.add(series);
152 series.addChangeListener(this);
153 fireDatasetChanged();
154 }
155
156 /**
157 * Adds any unique x-values from 'series' to the dataset, and also adds any
158 * x-values that are in the dataset but not in 'series' to the series.
159 *
160 * @param series the series (<code>null</code> not permitted).
161 */
162 private void updateXPoints(XYSeries series) {
163 if (series == null) {
164 throw new IllegalArgumentException("Null 'series' not permitted.");
165 }
166 HashSet seriesXPoints = new HashSet();
167 boolean savedState = this.propagateEvents;
168 this.propagateEvents = false;
169 for (int itemNo = 0; itemNo < series.getItemCount(); itemNo++) {
170 Number xValue = series.getX(itemNo);
171 seriesXPoints.add(xValue);
172 if (!this.xPoints.contains(xValue)) {
173 this.xPoints.add(xValue);
174 int seriesCount = this.data.size();
175 for (int seriesNo = 0; seriesNo < seriesCount; seriesNo++) {
176 XYSeries dataSeries = (XYSeries) this.data.get(seriesNo);
177 if (!dataSeries.equals(series)) {
178 dataSeries.add(xValue, null);
179 }
180 }
181 }
182 }
183 Iterator iterator = this.xPoints.iterator();
184 while (iterator.hasNext()) {
185 Number xPoint = (Number) iterator.next();
186 if (!seriesXPoints.contains(xPoint)) {
187 series.add(xPoint, null);
188 }
189 }
190 this.propagateEvents = savedState;
191 }
192
193 /**
194 * Updates the x-values for all the series in the dataset.
195 */
196 public void updateXPoints() {
197 this.propagateEvents = false;
198 for (int s = 0; s < this.data.size(); s++) {
199 updateXPoints((XYSeries) this.data.get(s));
200 }
201 if (this.autoPrune) {
202 prune();
203 }
204 this.propagateEvents = true;
205 }
206
207 /**
208 * Returns the number of series in the collection.
209 *
210 * @return The series count.
211 */
212 public int getSeriesCount() {
213 return this.data.size();
214 }
215
216 /**
217 * Returns the number of x values in the dataset.
218 *
219 * @return The number of x values in the dataset.
220 */
221 public int getItemCount() {
222 if (this.xPoints == null) {
223 return 0;
224 }
225 else {
226 return this.xPoints.size();
227 }
228 }
229
230 /**
231 * Returns a series.
232 *
233 * @param series the series (zero-based index).
234 *
235 * @return The series (never <code>null</code>).
236 */
237 public XYSeries getSeries(int series) {
238 if ((series < 0) || (series >= getSeriesCount())) {
239 throw new IllegalArgumentException("Index outside valid range.");
240 }
241 return (XYSeries) this.data.get(series);
242 }
243
244 /**
245 * Returns the key for a series.
246 *
247 * @param series the series (zero-based index).
248 *
249 * @return The key for a series.
250 */
251 public Comparable getSeriesKey(int series) {
252 // check arguments...delegated
253 return getSeries(series).getKey();
254 }
255
256 /**
257 * Returns the number of items in the specified series.
258 *
259 * @param series the series (zero-based index).
260 *
261 * @return The number of items in the specified series.
262 */
263 public int getItemCount(int series) {
264 // check arguments...delegated
265 return getSeries(series).getItemCount();
266 }
267
268 /**
269 * Returns the x-value for the specified series and item.
270 *
271 * @param series the series (zero-based index).
272 * @param item the item (zero-based index).
273 *
274 * @return The x-value for the specified series and item.
275 */
276 public Number getX(int series, int item) {
277 XYSeries s = (XYSeries) this.data.get(series);
278 XYDataItem dataItem = s.getDataItem(item);
279 return dataItem.getX();
280 }
281
282 /**
283 * Returns the starting X value for the specified series and item.
284 *
285 * @param series the series (zero-based index).
286 * @param item the item (zero-based index).
287 *
288 * @return The starting X value.
289 */
290 public Number getStartX(int series, int item) {
291 return this.intervalDelegate.getStartX(series, item);
292 }
293
294 /**
295 * Returns the ending X value for the specified series and item.
296 *
297 * @param series the series (zero-based index).
298 * @param item the item (zero-based index).
299 *
300 * @return The ending X value.
301 */
302 public Number getEndX(int series, int item) {
303 return this.intervalDelegate.getEndX(series, item);
304 }
305
306 /**
307 * Returns the y-value for the specified series and item.
308 *
309 * @param series the series (zero-based index).
310 * @param index the index of the item of interest (zero-based).
311 *
312 * @return The y-value for the specified series and item (possibly
313 * <code>null</code>).
314 */
315 public Number getY(int series, int index) {
316 XYSeries ts = (XYSeries) this.data.get(series);
317 XYDataItem dataItem = ts.getDataItem(index);
318 return dataItem.getY();
319 }
320
321 /**
322 * Returns the starting Y value for the specified series and item.
323 *
324 * @param series the series (zero-based index).
325 * @param item the item (zero-based index).
326 *
327 * @return The starting Y value.
328 */
329 public Number getStartY(int series, int item) {
330 return getY(series, item);
331 }
332
333 /**
334 * Returns the ending Y value for the specified series and item.
335 *
336 * @param series the series (zero-based index).
337 * @param item the item (zero-based index).
338 *
339 * @return The ending Y value.
340 */
341 public Number getEndY(int series, int item) {
342 return getY(series, item);
343 }
344
345 /**
346 * Removes all the series from the collection and sends a
347 * {@link DatasetChangeEvent} to all registered listeners.
348 */
349 public void removeAllSeries() {
350
351 // Unregister the collection as a change listener to each series in
352 // the collection.
353 for (int i = 0; i < this.data.size(); i++) {
354 XYSeries series = (XYSeries) this.data.get(i);
355 series.removeChangeListener(this);
356 }
357
358 // Remove all the series from the collection and notify listeners.
359 this.data.clear();
360 this.xPoints.clear();
361 fireDatasetChanged();
362 }
363
364 /**
365 * Removes a series from the collection and sends a
366 * {@link DatasetChangeEvent} to all registered listeners.
367 *
368 * @param series the series (<code>null</code> not permitted).
369 */
370 public void removeSeries(XYSeries series) {
371
372 // check arguments...
373 if (series == null) {
374 throw new IllegalArgumentException("Null 'series' argument.");
375 }
376
377 // remove the series...
378 if (this.data.contains(series)) {
379 series.removeChangeListener(this);
380 this.data.remove(series);
381 if (this.data.size() == 0) {
382 this.xPoints.clear();
383 }
384 fireDatasetChanged();
385 }
386
387 }
388
389 /**
390 * Removes a series from the collection and sends a
391 * {@link DatasetChangeEvent} to all registered listeners.
392 *
393 * @param series the series (zero based index).
394 */
395 public void removeSeries(int series) {
396
397 // check arguments...
398 if ((series < 0) || (series > getSeriesCount())) {
399 throw new IllegalArgumentException("Index outside valid range.");
400 }
401
402 // fetch the series, remove the change listener, then remove the series.
403 XYSeries s = (XYSeries) this.data.get(series);
404 s.removeChangeListener(this);
405 this.data.remove(series);
406 if (this.data.size() == 0) {
407 this.xPoints.clear();
408 }
409 else if (this.autoPrune) {
410 prune();
411 }
412 fireDatasetChanged();
413
414 }
415
416 /**
417 * Removes the items from all series for a given x value.
418 *
419 * @param x the x-value.
420 */
421 public void removeAllValuesForX(Number x) {
422 if (x == null) {
423 throw new IllegalArgumentException("Null 'x' argument.");
424 }
425 boolean savedState = this.propagateEvents;
426 this.propagateEvents = false;
427 for (int s = 0; s < this.data.size(); s++) {
428 XYSeries series = (XYSeries) this.data.get(s);
429 series.remove(x);
430 }
431 this.propagateEvents = savedState;
432 this.xPoints.remove(x);
433 fireDatasetChanged();
434 }
435
436 /**
437 * Returns <code>true</code> if all the y-values for the specified x-value
438 * are <code>null</code> and <code>false</code> otherwise.
439 *
440 * @param x the x-value.
441 *
442 * @return A boolean.
443 */
444 protected boolean canPrune(Number x) {
445 for (int s = 0; s < this.data.size(); s++) {
446 XYSeries series = (XYSeries) this.data.get(s);
447 if (series.getY(series.indexOf(x)) != null) {
448 return false;
449 }
450 }
451 return true;
452 }
453
454 /**
455 * Removes all x-values for which all the y-values are <code>null</code>.
456 */
457 public void prune() {
458 HashSet hs = (HashSet) this.xPoints.clone();
459 Iterator iterator = hs.iterator();
460 while (iterator.hasNext()) {
461 Number x = (Number) iterator.next();
462 if (canPrune(x)) {
463 removeAllValuesForX(x);
464 }
465 }
466 }
467
468 /**
469 * This method receives notification when a series belonging to the dataset
470 * changes. It responds by updating the x-points for the entire dataset
471 * and sending a {@link DatasetChangeEvent} to all registered listeners.
472 *
473 * @param event information about the change.
474 */
475 public void seriesChanged(SeriesChangeEvent event) {
476 if (this.propagateEvents) {
477 updateXPoints();
478 fireDatasetChanged();
479 }
480 }
481
482 /**
483 * Tests this collection for equality with an arbitrary object.
484 *
485 * @param obj the object (<code>null</code> permitted).
486 *
487 * @return A boolean.
488 */
489 public boolean equals(Object obj) {
490 if (obj == this) {
491 return true;
492 }
493 if (!(obj instanceof DefaultTableXYDataset)) {
494 return false;
495 }
496 DefaultTableXYDataset that = (DefaultTableXYDataset) obj;
497 if (this.autoPrune != that.autoPrune) {
498 return false;
499 }
500 if (this.propagateEvents != that.propagateEvents) {
501 return false;
502 }
503 if (!this.intervalDelegate.equals(that.intervalDelegate)) {
504 return false;
505 }
506 if (!ObjectUtilities.equal(this.data, that.data)) {
507 return false;
508 }
509 return true;
510 }
511
512 /**
513 * Returns a hash code.
514 *
515 * @return A hash code.
516 */
517 public int hashCode() {
518 int result;
519 result = (this.data != null ? this.data.hashCode() : 0);
520 result = 29 * result
521 + (this.xPoints != null ? this.xPoints.hashCode() : 0);
522 result = 29 * result + (this.propagateEvents ? 1 : 0);
523 result = 29 * result + (this.autoPrune ? 1 : 0);
524 return result;
525 }
526
527 /**
528 * Returns the minimum x-value in the dataset.
529 *
530 * @param includeInterval a flag that determines whether or not the
531 * x-interval is taken into account.
532 *
533 * @return The minimum value.
534 */
535 public double getDomainLowerBound(boolean includeInterval) {
536 return this.intervalDelegate.getDomainLowerBound(includeInterval);
537 }
538
539 /**
540 * Returns the maximum x-value in the dataset.
541 *
542 * @param includeInterval a flag that determines whether or not the
543 * x-interval is taken into account.
544 *
545 * @return The maximum value.
546 */
547 public double getDomainUpperBound(boolean includeInterval) {
548 return this.intervalDelegate.getDomainUpperBound(includeInterval);
549 }
550
551 /**
552 * Returns the range of the values in this dataset's domain.
553 *
554 * @param includeInterval a flag that determines whether or not the
555 * x-interval is taken into account.
556 *
557 * @return The range.
558 */
559 public Range getDomainBounds(boolean includeInterval) {
560 if (includeInterval) {
561 return this.intervalDelegate.getDomainBounds(includeInterval);
562 }
563 else {
564 return DatasetUtilities.iterateDomainBounds(this, includeInterval);
565 }
566 }
567
568 /**
569 * Returns the interval position factor.
570 *
571 * @return The interval position factor.
572 */
573 public double getIntervalPositionFactor() {
574 return this.intervalDelegate.getIntervalPositionFactor();
575 }
576
577 /**
578 * Sets the interval position factor. Must be between 0.0 and 1.0 inclusive.
579 * If the factor is 0.5, the gap is in the middle of the x values. If it
580 * is lesser than 0.5, the gap is farther to the left and if greater than
581 * 0.5 it gets farther to the right.
582 *
583 * @param d the new interval position factor.
584 */
585 public void setIntervalPositionFactor(double d) {
586 this.intervalDelegate.setIntervalPositionFactor(d);
587 fireDatasetChanged();
588 }
589
590 /**
591 * returns the full interval width.
592 *
593 * @return The interval width to use.
594 */
595 public double getIntervalWidth() {
596 return this.intervalDelegate.getIntervalWidth();
597 }
598
599 /**
600 * Sets the interval width to a fixed value, and sends a
601 * {@link DatasetChangeEvent} to all registered listeners.
602 *
603 * @param d the new interval width (must be > 0).
604 */
605 public void setIntervalWidth(double d) {
606 this.intervalDelegate.setFixedIntervalWidth(d);
607 fireDatasetChanged();
608 }
609
610 /**
611 * Returns whether the interval width is automatically calculated or not.
612 *
613 * @return A flag that determines whether or not the interval width is
614 * automatically calculated.
615 */
616 public boolean isAutoWidth() {
617 return this.intervalDelegate.isAutoWidth();
618 }
619
620 /**
621 * Sets the flag that indicates whether the interval width is automatically
622 * calculated or not.
623 *
624 * @param b a boolean.
625 */
626 public void setAutoWidth(boolean b) {
627 this.intervalDelegate.setAutoWidth(b);
628 fireDatasetChanged();
629 }
630
631 }