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 }