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