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