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 * DefaultIntervalCategoryDataset.java
029 * -----------------------------------
030 * (C) Copyright 2002-2007, by Jeremy Bowman and Contributors.
031 *
032 * Original Author: Jeremy Bowman;
033 * Contributor(s): David Gilbert (for Object Refinery Limited);
034 *
035 * Changes
036 * -------
037 * 29-Apr-2002 : Version 1, contributed by Jeremy Bowman (DG);
038 * 24-Oct-2002 : Amendments for changes made to the dataset interface (DG);
039 * ------------- JFREECHART 1.0.x ---------------------------------------------
040 * 08-Mar-2007 : Added equals() and clone() overrides (DG);
041 *
042 */
043
044 package org.jfree.data.category;
045
046 import java.util.ArrayList;
047 import java.util.Arrays;
048 import java.util.Collections;
049 import java.util.List;
050 import java.util.ResourceBundle;
051
052 import org.jfree.data.DataUtilities;
053 import org.jfree.data.UnknownKeyException;
054 import org.jfree.data.general.AbstractSeriesDataset;
055
056 /**
057 * A convenience class that provides a default implementation of the
058 * {@link IntervalCategoryDataset} interface.
059 * <p>
060 * The standard constructor accepts data in a two dimensional array where the
061 * first dimension is the series, and the second dimension is the category.
062 */
063 public class DefaultIntervalCategoryDataset extends AbstractSeriesDataset
064 implements IntervalCategoryDataset {
065
066 /** The series keys. */
067 private Comparable[] seriesKeys;
068
069 /** The category keys. */
070 private Comparable[] categoryKeys;
071
072 /** Storage for the start value data. */
073 private Number[][] startData;
074
075 /** Storage for the end value data. */
076 private Number[][] endData;
077
078 /**
079 * Creates a new dataset.
080 *
081 * @param starts the starting values for the intervals.
082 * @param ends the ending values for the intervals.
083 */
084 public DefaultIntervalCategoryDataset(double[][] starts, double[][] ends) {
085 this(DataUtilities.createNumberArray2D(starts),
086 DataUtilities.createNumberArray2D(ends));
087 }
088
089 /**
090 * Constructs a dataset and populates it with data from the array.
091 * <p>
092 * The arrays are indexed as data[series][category]. Series and category
093 * names are automatically generated - you can change them using the
094 * {@link #setSeriesKeys(Comparable[])} and
095 * {@link #setCategoryKeys(Comparable[])} methods.
096 *
097 * @param starts the start values data.
098 * @param ends the end values data.
099 */
100 public DefaultIntervalCategoryDataset(Number[][] starts, Number[][] ends) {
101 this(null, null, starts, ends);
102 }
103
104 /**
105 * Constructs a DefaultIntervalCategoryDataset, populates it with data
106 * from the arrays, and uses the supplied names for the series.
107 * <p>
108 * Category names are generated automatically ("Category 1", "Category 2",
109 * etc).
110 *
111 * @param seriesNames the series names.
112 * @param starts the start values data, indexed as data[series][category].
113 * @param ends the end values data, indexed as data[series][category].
114 */
115 public DefaultIntervalCategoryDataset(String[] seriesNames,
116 Number[][] starts,
117 Number[][] ends) {
118
119 this(seriesNames, null, starts, ends);
120
121 }
122
123 /**
124 * Constructs a DefaultIntervalCategoryDataset, populates it with data
125 * from the arrays, and uses the supplied names for the series and the
126 * supplied objects for the categories.
127 *
128 * @param seriesKeys the series keys.
129 * @param categoryKeys the categories.
130 * @param starts the start values data, indexed as data[series][category].
131 * @param ends the end values data, indexed as data[series][category].
132 */
133 public DefaultIntervalCategoryDataset(Comparable[] seriesKeys,
134 Comparable[] categoryKeys,
135 Number[][] starts,
136 Number[][] ends) {
137
138 this.startData = starts;
139 this.endData = ends;
140
141 if (starts != null && ends != null) {
142
143 String baseName = "org.jfree.data.resources.DataPackageResources";
144 ResourceBundle resources = ResourceBundle.getBundle(baseName);
145
146 int seriesCount = starts.length;
147 if (seriesCount != ends.length) {
148 String errMsg = "DefaultIntervalCategoryDataset: the number "
149 + "of series in the start value dataset does "
150 + "not match the number of series in the end "
151 + "value dataset.";
152 throw new IllegalArgumentException(errMsg);
153 }
154 if (seriesCount > 0) {
155
156 // set up the series names...
157 if (seriesKeys != null) {
158
159 if (seriesKeys.length != seriesCount) {
160 throw new IllegalArgumentException(
161 "The number of series keys does not "
162 + "match the number of series in the data.");
163 }
164
165 this.seriesKeys = seriesKeys;
166 }
167 else {
168 String prefix = resources.getString(
169 "series.default-prefix") + " ";
170 this.seriesKeys = generateKeys(seriesCount, prefix);
171 }
172
173 // set up the category names...
174 int categoryCount = starts[0].length;
175 if (categoryCount != ends[0].length) {
176 String errMsg = "DefaultIntervalCategoryDataset: the "
177 + "number of categories in the start value "
178 + "dataset does not match the number of "
179 + "categories in the end value dataset.";
180 throw new IllegalArgumentException(errMsg);
181 }
182 if (categoryKeys != null) {
183 if (categoryKeys.length != categoryCount) {
184 throw new IllegalArgumentException(
185 "The number of category keys does not match "
186 + "the number of categories in the data.");
187 }
188 this.categoryKeys = categoryKeys;
189 }
190 else {
191 String prefix = resources.getString(
192 "categories.default-prefix") + " ";
193 this.categoryKeys = generateKeys(categoryCount, prefix);
194 }
195
196 }
197 else {
198 this.seriesKeys = null;
199 this.categoryKeys = null;
200 }
201 }
202
203 }
204
205 /**
206 * Returns the number of series in the dataset (possibly zero).
207 *
208 * @return The number of series in the dataset.
209 *
210 * @see #getRowCount()
211 * @see #getCategoryCount()
212 */
213 public int getSeriesCount() {
214 int result = 0;
215 if (this.startData != null) {
216 result = this.startData.length;
217 }
218 return result;
219 }
220
221 /**
222 * Returns a series index.
223 *
224 * @param seriesKey the series key.
225 *
226 * @return The series index.
227 *
228 * @see #getRowIndex(Comparable)
229 * @see #getSeriesKey(int)
230 */
231 public int getSeriesIndex(Comparable seriesKey) {
232 int result = -1;
233 for (int i = 0; i < this.seriesKeys.length; i++) {
234 if (seriesKey.equals(this.seriesKeys[i])) {
235 result = i;
236 break;
237 }
238 }
239 return result;
240 }
241
242 /**
243 * Returns the name of the specified series.
244 *
245 * @param series the index of the required series (zero-based).
246 *
247 * @return The name of the specified series.
248 *
249 * @see #getSeriesIndex(Comparable)
250 */
251 public Comparable getSeriesKey(int series) {
252 if ((series >= getSeriesCount()) || (series < 0)) {
253 throw new IllegalArgumentException("No such series : " + series);
254 }
255 return this.seriesKeys[series];
256 }
257
258 /**
259 * Sets the names of the series in the dataset.
260 *
261 * @param seriesKeys the new keys (<code>null</code> not permitted, the
262 * length of the array must match the number of series in the
263 * dataset).
264 *
265 * @see #setCategoryKeys(Comparable[])
266 */
267 public void setSeriesKeys(Comparable[] seriesKeys) {
268 if (seriesKeys == null) {
269 throw new IllegalArgumentException("Null 'seriesKeys' argument.");
270 }
271 if (seriesKeys.length != getSeriesCount()) {
272 throw new IllegalArgumentException(
273 "The number of series keys does not match the data.");
274 }
275 this.seriesKeys = seriesKeys;
276 fireDatasetChanged();
277 }
278
279 /**
280 * Returns the number of categories in the dataset.
281 *
282 * @return The number of categories in the dataset.
283 *
284 * @see #getColumnCount()
285 */
286 public int getCategoryCount() {
287 int result = 0;
288 if (this.startData != null) {
289 if (getSeriesCount() > 0) {
290 result = this.startData[0].length;
291 }
292 }
293 return result;
294 }
295
296 /**
297 * Returns a list of the categories in the dataset. This method supports
298 * the {@link CategoryDataset} interface.
299 *
300 * @return A list of the categories in the dataset.
301 *
302 * @see #getRowKeys()
303 */
304 public List getColumnKeys() {
305 // the CategoryDataset interface expects a list of categories, but
306 // we've stored them in an array...
307 if (this.categoryKeys == null) {
308 return new ArrayList();
309 }
310 else {
311 return Collections.unmodifiableList(Arrays.asList(
312 this.categoryKeys));
313 }
314 }
315
316 /**
317 * Sets the categories for the dataset.
318 *
319 * @param categoryKeys an array of objects representing the categories in
320 * the dataset.
321 *
322 * @see #getRowKeys()
323 * @see #setSeriesKeys(Comparable[])
324 */
325 public void setCategoryKeys(Comparable[] categoryKeys) {
326 if (categoryKeys == null) {
327 throw new IllegalArgumentException("Null 'categoryKeys' argument.");
328 }
329 if (categoryKeys.length != this.startData[0].length) {
330 throw new IllegalArgumentException(
331 "The number of categories does not match the data.");
332 }
333 for (int i = 0; i < categoryKeys.length; i++) {
334 if (categoryKeys[i] == null) {
335 throw new IllegalArgumentException(
336 "DefaultIntervalCategoryDataset.setCategoryKeys(): "
337 + "null category not permitted.");
338 }
339 }
340 this.categoryKeys = categoryKeys;
341 fireDatasetChanged();
342 }
343
344 /**
345 * Returns the data value for one category in a series.
346 * <P>
347 * This method is part of the CategoryDataset interface. Not particularly
348 * meaningful for this class...returns the end value.
349 *
350 * @param series The required series (zero based index).
351 * @param category The required category.
352 *
353 * @return The data value for one category in a series (null possible).
354 *
355 * @see #getEndValue(Comparable, Comparable)
356 */
357 public Number getValue(Comparable series, Comparable category) {
358 int seriesIndex = getSeriesIndex(series);
359 if (seriesIndex < 0) {
360 throw new UnknownKeyException("Unknown 'series' key.");
361 }
362 int itemIndex = getColumnIndex(category);
363 if (itemIndex < 0) {
364 throw new UnknownKeyException("Unknown 'category' key.");
365 }
366 return getValue(seriesIndex, itemIndex);
367 }
368
369 /**
370 * Returns the data value for one category in a series.
371 * <P>
372 * This method is part of the CategoryDataset interface. Not particularly
373 * meaningful for this class...returns the end value.
374 *
375 * @param series the required series (zero based index).
376 * @param category the required category.
377 *
378 * @return The data value for one category in a series (null possible).
379 *
380 * @see #getEndValue(int, int)
381 */
382 public Number getValue(int series, int category) {
383 return getEndValue(series, category);
384 }
385
386 /**
387 * Returns the start data value for one category in a series.
388 *
389 * @param series the required series.
390 * @param category the required category.
391 *
392 * @return The start data value for one category in a series
393 * (possibly <code>null</code>).
394 *
395 * @see #getStartValue(int, int)
396 */
397 public Number getStartValue(Comparable series, Comparable category) {
398 int seriesIndex = getSeriesIndex(series);
399 if (seriesIndex < 0) {
400 throw new UnknownKeyException("Unknown 'series' key.");
401 }
402 int itemIndex = getColumnIndex(category);
403 if (itemIndex < 0) {
404 throw new UnknownKeyException("Unknown 'category' key.");
405 }
406 return getStartValue(seriesIndex, itemIndex);
407 }
408
409 /**
410 * Returns the start data value for one category in a series.
411 *
412 * @param series the required series (zero based index).
413 * @param category the required category.
414 *
415 * @return The start data value for one category in a series
416 * (possibly <code>null</code>).
417 *
418 * @see #getStartValue(Comparable, Comparable)
419 */
420 public Number getStartValue(int series, int category) {
421
422 // check arguments...
423 if ((series < 0) || (series >= getSeriesCount())) {
424 throw new IllegalArgumentException(
425 "DefaultIntervalCategoryDataset.getValue(): "
426 + "series index out of range.");
427 }
428
429 if ((category < 0) || (category >= getCategoryCount())) {
430 throw new IllegalArgumentException(
431 "DefaultIntervalCategoryDataset.getValue(): "
432 + "category index out of range.");
433 }
434
435 // fetch the value...
436 return this.startData[series][category];
437
438 }
439
440 /**
441 * Returns the end data value for one category in a series.
442 *
443 * @param series the required series.
444 * @param category the required category.
445 *
446 * @return The end data value for one category in a series (null possible).
447 *
448 * @see #getEndValue(int, int)
449 */
450 public Number getEndValue(Comparable series, Comparable category) {
451 int seriesIndex = getSeriesIndex(series);
452 if (seriesIndex < 0) {
453 throw new UnknownKeyException("Unknown 'series' key.");
454 }
455 int itemIndex = getColumnIndex(category);
456 if (itemIndex < 0) {
457 throw new UnknownKeyException("Unknown 'category' key.");
458 }
459 return getEndValue(seriesIndex, itemIndex);
460 }
461
462 /**
463 * Returns the end data value for one category in a series.
464 *
465 * @param series the required series (zero based index).
466 * @param category the required category.
467 *
468 * @return The end data value for one category in a series (null possible).
469 *
470 * @see #getEndValue(Comparable, Comparable)
471 */
472 public Number getEndValue(int series, int category) {
473 if ((series < 0) || (series >= getSeriesCount())) {
474 throw new IllegalArgumentException(
475 "DefaultIntervalCategoryDataset.getValue(): "
476 + "series index out of range.");
477 }
478
479 if ((category < 0) || (category >= getCategoryCount())) {
480 throw new IllegalArgumentException(
481 "DefaultIntervalCategoryDataset.getValue(): "
482 + "category index out of range.");
483 }
484
485 return this.endData[series][category];
486 }
487
488 /**
489 * Sets the start data value for one category in a series.
490 *
491 * @param series the series (zero-based index).
492 * @param category the category.
493 *
494 * @param value The value.
495 *
496 * @see #setEndValue(int, Comparable, Number)
497 */
498 public void setStartValue(int series, Comparable category, Number value) {
499
500 // does the series exist?
501 if ((series < 0) || (series > getSeriesCount() - 1)) {
502 throw new IllegalArgumentException(
503 "DefaultIntervalCategoryDataset.setValue: "
504 + "series outside valid range.");
505 }
506
507 // is the category valid?
508 int categoryIndex = getCategoryIndex(category);
509 if (categoryIndex < 0) {
510 throw new IllegalArgumentException(
511 "DefaultIntervalCategoryDataset.setValue: "
512 + "unrecognised category.");
513 }
514
515 // update the data...
516 this.startData[series][categoryIndex] = value;
517 fireDatasetChanged();
518
519 }
520
521 /**
522 * Sets the end data value for one category in a series.
523 *
524 * @param series the series (zero-based index).
525 * @param category the category.
526 *
527 * @param value the value.
528 *
529 * @see #setStartValue(int, Comparable, Number)
530 */
531 public void setEndValue(int series, Comparable category, Number value) {
532
533 // does the series exist?
534 if ((series < 0) || (series > getSeriesCount() - 1)) {
535 throw new IllegalArgumentException(
536 "DefaultIntervalCategoryDataset.setValue: "
537 + "series outside valid range.");
538 }
539
540 // is the category valid?
541 int categoryIndex = getCategoryIndex(category);
542 if (categoryIndex < 0) {
543 throw new IllegalArgumentException(
544 "DefaultIntervalCategoryDataset.setValue: "
545 + "unrecognised category.");
546 }
547
548 // update the data...
549 this.endData[series][categoryIndex] = value;
550 fireDatasetChanged();
551
552 }
553
554 /**
555 * Returns the index for the given category.
556 *
557 * @param category the category (<code>null</code> not permitted).
558 *
559 * @return The index.
560 *
561 * @see #getColumnIndex(Comparable)
562 */
563 public int getCategoryIndex(Comparable category) {
564 int result = -1;
565 for (int i = 0; i < this.categoryKeys.length; i++) {
566 if (category.equals(this.categoryKeys[i])) {
567 result = i;
568 break;
569 }
570 }
571 return result;
572 }
573
574 /**
575 * Generates an array of keys, by appending a space plus an integer
576 * (starting with 1) to the supplied prefix string.
577 *
578 * @param count the number of keys required.
579 * @param prefix the name prefix.
580 *
581 * @return An array of <i>prefixN</i> with N = { 1 .. count}.
582 */
583 private Comparable[] generateKeys(int count, String prefix) {
584 Comparable[] result = new Comparable[count];
585 String name;
586 for (int i = 0; i < count; i++) {
587 name = prefix + (i + 1);
588 result[i] = name;
589 }
590 return result;
591 }
592
593 /**
594 * Returns a column key.
595 *
596 * @param column the column index.
597 *
598 * @return The column key.
599 *
600 * @see #getRowKey(int)
601 */
602 public Comparable getColumnKey(int column) {
603 return this.categoryKeys[column];
604 }
605
606 /**
607 * Returns a column index.
608 *
609 * @param columnKey the column key (<code>null</code> not permitted).
610 *
611 * @return The column index.
612 *
613 * @see #getCategoryIndex(Comparable)
614 */
615 public int getColumnIndex(Comparable columnKey) {
616 if (columnKey == null) {
617 throw new IllegalArgumentException("Null 'columnKey' argument.");
618 }
619 return getCategoryIndex(columnKey);
620 }
621
622 /**
623 * Returns a row index.
624 *
625 * @param rowKey the row key.
626 *
627 * @return The row index.
628 *
629 * @see #getSeriesIndex(Comparable)
630 */
631 public int getRowIndex(Comparable rowKey) {
632 return getSeriesIndex(rowKey);
633 }
634
635 /**
636 * Returns a list of the series in the dataset. This method supports the
637 * {@link CategoryDataset} interface.
638 *
639 * @return A list of the series in the dataset.
640 *
641 * @see #getColumnKeys()
642 */
643 public List getRowKeys() {
644 // the CategoryDataset interface expects a list of series, but
645 // we've stored them in an array...
646 if (this.seriesKeys == null) {
647 return new java.util.ArrayList();
648 }
649 else {
650 return Collections.unmodifiableList(Arrays.asList(this.seriesKeys));
651 }
652 }
653
654 /**
655 * Returns the name of the specified series.
656 *
657 * @param row the index of the required row/series (zero-based).
658 *
659 * @return The name of the specified series.
660 *
661 * @see #getColumnKey(int)
662 */
663 public Comparable getRowKey(int row) {
664 if ((row >= getRowCount()) || (row < 0)) {
665 throw new IllegalArgumentException(
666 "The 'row' argument is out of bounds.");
667 }
668 return this.seriesKeys[row];
669 }
670
671 /**
672 * Returns the number of categories in the dataset. This method is part of
673 * the {@link CategoryDataset} interface.
674 *
675 * @return The number of categories in the dataset.
676 *
677 * @see #getCategoryCount()
678 * @see #getRowCount()
679 */
680 public int getColumnCount() {
681 return this.categoryKeys.length;
682 }
683
684 /**
685 * Returns the number of series in the dataset (possibly zero).
686 *
687 * @return The number of series in the dataset.
688 *
689 * @see #getSeriesCount()
690 * @see #getColumnCount()
691 */
692 public int getRowCount() {
693 return this.seriesKeys.length;
694 }
695
696 /**
697 * Tests this dataset for equality with an arbitrary object.
698 *
699 * @param obj the object (<code>null</code> permitted).
700 *
701 * @return A boolean.
702 */
703 public boolean equals(Object obj) {
704 if (obj == this) {
705 return true;
706 }
707 if (!(obj instanceof DefaultIntervalCategoryDataset)) {
708 return false;
709 }
710 DefaultIntervalCategoryDataset that
711 = (DefaultIntervalCategoryDataset) obj;
712 if (!Arrays.equals(this.seriesKeys, that.seriesKeys)) {
713 return false;
714 }
715 if (!Arrays.equals(this.categoryKeys, that.categoryKeys)) {
716 return false;
717 }
718 if (!equal(this.startData, that.startData)) {
719 return false;
720 }
721 if (!equal(this.endData, that.endData)) {
722 return false;
723 }
724 // seem to be the same...
725 return true;
726 }
727
728 /**
729 * Returns a clone of this dataset.
730 *
731 * @return A clone.
732 *
733 * @throws CloneNotSupportedException if there is a problem cloning the
734 * dataset.
735 */
736 public Object clone() throws CloneNotSupportedException {
737 DefaultIntervalCategoryDataset clone
738 = (DefaultIntervalCategoryDataset) super.clone();
739 clone.categoryKeys = (Comparable[]) this.categoryKeys.clone();
740 clone.seriesKeys = (Comparable[]) this.seriesKeys.clone();
741 clone.startData = clone(this.startData);
742 clone.endData = clone(this.endData);
743 return clone;
744 }
745
746 /**
747 * Tests two double[][] arrays for equality.
748 *
749 * @param array1 the first array (<code>null</code> permitted).
750 * @param array2 the second arrray (<code>null</code> permitted).
751 *
752 * @return A boolean.
753 */
754 private static boolean equal(Number[][] array1, Number[][] array2) {
755 if (array1 == null) {
756 return (array2 == null);
757 }
758 if (array2 == null) {
759 return false;
760 }
761 if (array1.length != array2.length) {
762 return false;
763 }
764 for (int i = 0; i < array1.length; i++) {
765 if (!Arrays.equals(array1[i], array2[i])) {
766 return false;
767 }
768 }
769 return true;
770 }
771
772 /**
773 * Clones a two dimensional array of <code>Number</code> objects.
774 *
775 * @param array the array (<code>null</code> not permitted).
776 *
777 * @return A clone of the array.
778 */
779 private static Number[][] clone(Number[][] array) {
780 if (array == null) {
781 throw new IllegalArgumentException("Null 'array' argument.");
782 }
783 Number[][] result = new Number[array.length][];
784 for (int i = 0; i < array.length; i++) {
785 Number[] child = array[i];
786 Number[] copychild = new Number[child.length];
787 System.arraycopy(child, 0, copychild, 0, child.length);
788 result[i] = copychild;
789 }
790 return result;
791 }
792
793 /**
794 * Returns a list of the series in the dataset.
795 *
796 * @return A list of the series in the dataset.
797 *
798 * @deprecated Use {@link #getRowKeys()} instead.
799 */
800 public List getSeries() {
801 if (this.seriesKeys == null) {
802 return new java.util.ArrayList();
803 }
804 else {
805 return Collections.unmodifiableList(Arrays.asList(this.seriesKeys));
806 }
807 }
808
809 /**
810 * Returns a list of the categories in the dataset.
811 *
812 * @return A list of the categories in the dataset.
813 *
814 * @deprecated Use {@link #getColumnKeys()} instead.
815 */
816 public List getCategories() {
817 return getColumnKeys();
818 }
819
820 /**
821 * Returns the item count.
822 *
823 * @return The item count.
824 *
825 * @deprecated Use {@link #getCategoryCount()} instead.
826 */
827 public int getItemCount() {
828 return this.categoryKeys.length;
829 }
830
831 }