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 }