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