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