/* =========================================================== * JFreeChart : a free chart library for the Java(tm) platform * =========================================================== * * (C) Copyright 2000-2011, by Object Refinery Limited and Contributors. * * Project Info: http://www.jfree.org/jfreechart/index.html * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA. * * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners.] * * ----------------------------------- * DefaultIntervalCategoryDataset.java * ----------------------------------- * (C) Copyright 2002-2008, by Jeremy Bowman and Contributors. * * Original Author: Jeremy Bowman; * Contributor(s): David Gilbert (for Object Refinery Limited); * * Changes * ------- * 29-Apr-2002 : Version 1, contributed by Jeremy Bowman (DG); * 24-Oct-2002 : Amendments for changes made to the dataset interface (DG); * ------------- JFREECHART 1.0.x --------------------------------------------- * 08-Mar-2007 : Added equals() and clone() overrides (DG); * 25-Feb-2008 : Fix for the special case where the dataset is empty, see bug * 1897580 (DG) * 18-Dec-2008 : Use ResourceBundleWrapper - see patch 1607918 by * Jess Thrysoee (DG); * */ package org.jfree.data.category; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.ResourceBundle; import org.jfree.chart.util.ResourceBundleWrapper; import org.jfree.data.DataUtilities; import org.jfree.data.UnknownKeyException; import org.jfree.data.general.AbstractSeriesDataset; /** * A convenience class that provides a default implementation of the * {@link IntervalCategoryDataset} interface. * <p> * The standard constructor accepts data in a two dimensional array where the * first dimension is the series, and the second dimension is the category. */ public class DefaultIntervalCategoryDataset extends AbstractSeriesDataset implements IntervalCategoryDataset { /** The series keys. */ private Comparable[] seriesKeys; /** The category keys. */ private Comparable[] categoryKeys; /** Storage for the start value data. */ private Number[][] startData; /** Storage for the end value data. */ private Number[][] endData; /** * Creates a new dataset using the specified data values and automatically * generated series and category keys. * * @param starts the starting values for the intervals (<code>null</code> * not permitted). * @param ends the ending values for the intervals (<code>null</code> not * permitted). */ public DefaultIntervalCategoryDataset(double[][] starts, double[][] ends) { this(DataUtilities.createNumberArray2D(starts), DataUtilities.createNumberArray2D(ends)); } /** * Constructs a dataset and populates it with data from the array. * <p> * The arrays are indexed as data[series][category]. Series and category * names are automatically generated - you can change them using the * {@link #setSeriesKeys(Comparable[])} and * {@link #setCategoryKeys(Comparable[])} methods. * * @param starts the start values data. * @param ends the end values data. */ public DefaultIntervalCategoryDataset(Number[][] starts, Number[][] ends) { this(null, null, starts, ends); } /** * Constructs a DefaultIntervalCategoryDataset, populates it with data * from the arrays, and uses the supplied names for the series. * <p> * Category names are generated automatically ("Category 1", "Category 2", * etc). * * @param seriesNames the series names (if <code>null</code>, series names * will be generated automatically). * @param starts the start values data, indexed as data[series][category]. * @param ends the end values data, indexed as data[series][category]. */ public DefaultIntervalCategoryDataset(String[] seriesNames, Number[][] starts, Number[][] ends) { this(seriesNames, null, starts, ends); } /** * Constructs a DefaultIntervalCategoryDataset, populates it with data * from the arrays, and uses the supplied names for the series and the * supplied objects for the categories. * * @param seriesKeys the series keys (if <code>null</code>, series keys * will be generated automatically). * @param categoryKeys the category keys (if <code>null</code>, category * keys will be generated automatically). * @param starts the start values data, indexed as data[series][category]. * @param ends the end values data, indexed as data[series][category]. */ public DefaultIntervalCategoryDataset(Comparable[] seriesKeys, Comparable[] categoryKeys, Number[][] starts, Number[][] ends) { this.startData = starts; this.endData = ends; if (starts != null && ends != null) { String baseName = "org.jfree.data.resources.DataPackageResources"; ResourceBundle resources = ResourceBundleWrapper.getBundle( baseName); int seriesCount = starts.length; if (seriesCount != ends.length) { String errMsg = "DefaultIntervalCategoryDataset: the number " + "of series in the start value dataset does " + "not match the number of series in the end " + "value dataset."; throw new IllegalArgumentException(errMsg); } if (seriesCount > 0) { // set up the series names... if (seriesKeys != null) { if (seriesKeys.length != seriesCount) { throw new IllegalArgumentException( "The number of series keys does not " + "match the number of series in the data."); } this.seriesKeys = seriesKeys; } else { String prefix = resources.getString( "series.default-prefix") + " "; this.seriesKeys = generateKeys(seriesCount, prefix); } // set up the category names... int categoryCount = starts[0].length; if (categoryCount != ends[0].length) { String errMsg = "DefaultIntervalCategoryDataset: the " + "number of categories in the start value " + "dataset does not match the number of " + "categories in the end value dataset."; throw new IllegalArgumentException(errMsg); } if (categoryKeys != null) { if (categoryKeys.length != categoryCount) { throw new IllegalArgumentException( "The number of category keys does not match " + "the number of categories in the data."); } this.categoryKeys = categoryKeys; } else { String prefix = resources.getString( "categories.default-prefix") + " "; this.categoryKeys = generateKeys(categoryCount, prefix); } } else { this.seriesKeys = new Comparable[0]; this.categoryKeys = new Comparable[0]; } } } /** * Returns the number of series in the dataset (possibly zero). * * @return The number of series in the dataset. * * @see #getRowCount() * @see #getCategoryCount() */ @Override public int getSeriesCount() { int result = 0; if (this.startData != null) { result = this.startData.length; } return result; } /** * Returns a series index. * * @param seriesKey the series key. * * @return The series index. * * @see #getRowIndex(Comparable) * @see #getSeriesKey(int) */ public int getSeriesIndex(Comparable seriesKey) { int result = -1; for (int i = 0; i < this.seriesKeys.length; i++) { if (seriesKey.equals(this.seriesKeys[i])) { result = i; break; } } return result; } /** * Returns the name of the specified series. * * @param series the index of the required series (zero-based). * * @return The name of the specified series. * * @see #getSeriesIndex(Comparable) */ @Override public Comparable getSeriesKey(int series) { if ((series >= getSeriesCount()) || (series < 0)) { throw new IllegalArgumentException("No such series : " + series); } return this.seriesKeys[series]; } /** * Sets the names of the series in the dataset. * * @param seriesKeys the new keys (<code>null</code> not permitted, the * length of the array must match the number of series in the * dataset). * * @see #setCategoryKeys(Comparable[]) */ public void setSeriesKeys(Comparable[] seriesKeys) { if (seriesKeys == null) { throw new IllegalArgumentException("Null 'seriesKeys' argument."); } if (seriesKeys.length != getSeriesCount()) { throw new IllegalArgumentException( "The number of series keys does not match the data."); } this.seriesKeys = seriesKeys; fireDatasetChanged(); } /** * Returns the number of categories in the dataset. * * @return The number of categories in the dataset. * * @see #getColumnCount() */ public int getCategoryCount() { int result = 0; if (this.startData != null) { if (getSeriesCount() > 0) { result = this.startData[0].length; } } return result; } /** * Returns a list of the categories in the dataset. This method supports * the {@link CategoryDataset} interface. * * @return A list of the categories in the dataset. * * @see #getRowKeys() */ @Override public List<Comparable> getColumnKeys() { // the CategoryDataset interface expects a list of categories, but // we've stored them in an array... if (this.categoryKeys == null) { return new ArrayList<Comparable>(); } else { return Collections.unmodifiableList(Arrays.asList( this.categoryKeys)); } } /** * Sets the categories for the dataset. * * @param categoryKeys an array of objects representing the categories in * the dataset. * * @see #getRowKeys() * @see #setSeriesKeys(Comparable[]) */ public void setCategoryKeys(Comparable[] categoryKeys) { if (categoryKeys == null) { throw new IllegalArgumentException("Null 'categoryKeys' argument."); } if (categoryKeys.length != getCategoryCount()) { throw new IllegalArgumentException( "The number of categories does not match the data."); } for (Comparable categoryKey : categoryKeys) { if (categoryKey == null) { throw new IllegalArgumentException( "DefaultIntervalCategoryDataset.setCategoryKeys(): " + "null category not permitted."); } } this.categoryKeys = categoryKeys; fireDatasetChanged(); } /** * Returns the data value for one category in a series. * <P> * This method is part of the CategoryDataset interface. Not particularly * meaningful for this class...returns the end value. * * @param series The required series (zero based index). * @param category The required category. * * @return The data value for one category in a series (null possible). * * @see #getEndValue(Comparable, Comparable) */ @Override public Number getValue(Comparable series, Comparable category) { int seriesIndex = getSeriesIndex(series); if (seriesIndex < 0) { throw new UnknownKeyException("Unknown 'series' key."); } int itemIndex = getColumnIndex(category); if (itemIndex < 0) { throw new UnknownKeyException("Unknown 'category' key."); } return getValue(seriesIndex, itemIndex); } /** * Returns the data value for one category in a series. * <P> * This method is part of the CategoryDataset interface. Not particularly * meaningful for this class...returns the end value. * * @param series the required series (zero based index). * @param category the required category. * * @return The data value for one category in a series (null possible). * * @see #getEndValue(int, int) */ @Override public Number getValue(int series, int category) { return getEndValue(series, category); } /** * Returns the start data value for one category in a series. * * @param series the required series. * @param category the required category. * * @return The start data value for one category in a series * (possibly <code>null</code>). * * @see #getStartValue(int, int) */ @Override public Number getStartValue(Comparable series, Comparable category) { int seriesIndex = getSeriesIndex(series); if (seriesIndex < 0) { throw new UnknownKeyException("Unknown 'series' key."); } int itemIndex = getColumnIndex(category); if (itemIndex < 0) { throw new UnknownKeyException("Unknown 'category' key."); } return getStartValue(seriesIndex, itemIndex); } /** * Returns the start data value for one category in a series. * * @param series the required series (zero based index). * @param category the required category. * * @return The start data value for one category in a series * (possibly <code>null</code>). * * @see #getStartValue(Comparable, Comparable) */ @Override public Number getStartValue(int series, int category) { // check arguments... if ((series < 0) || (series >= getSeriesCount())) { throw new IllegalArgumentException( "DefaultIntervalCategoryDataset.getValue(): " + "series index out of range."); } if ((category < 0) || (category >= getCategoryCount())) { throw new IllegalArgumentException( "DefaultIntervalCategoryDataset.getValue(): " + "category index out of range."); } // fetch the value... return this.startData[series][category]; } /** * Returns the end data value for one category in a series. * * @param series the required series. * @param category the required category. * * @return The end data value for one category in a series (null possible). * * @see #getEndValue(int, int) */ @Override public Number getEndValue(Comparable series, Comparable category) { int seriesIndex = getSeriesIndex(series); if (seriesIndex < 0) { throw new UnknownKeyException("Unknown 'series' key."); } int itemIndex = getColumnIndex(category); if (itemIndex < 0) { throw new UnknownKeyException("Unknown 'category' key."); } return getEndValue(seriesIndex, itemIndex); } /** * Returns the end data value for one category in a series. * * @param series the required series (zero based index). * @param category the required category. * * @return The end data value for one category in a series (null possible). * * @see #getEndValue(Comparable, Comparable) */ @Override public Number getEndValue(int series, int category) { if ((series < 0) || (series >= getSeriesCount())) { throw new IllegalArgumentException( "DefaultIntervalCategoryDataset.getValue(): " + "series index out of range."); } if ((category < 0) || (category >= getCategoryCount())) { throw new IllegalArgumentException( "DefaultIntervalCategoryDataset.getValue(): " + "category index out of range."); } return this.endData[series][category]; } /** * Sets the start data value for one category in a series. * * @param series the series (zero-based index). * @param category the category. * * @param value The value. * * @see #setEndValue(int, Comparable, Number) */ public void setStartValue(int series, Comparable category, Number value) { // does the series exist? if ((series < 0) || (series > getSeriesCount() - 1)) { throw new IllegalArgumentException( "DefaultIntervalCategoryDataset.setValue: " + "series outside valid range."); } // is the category valid? int categoryIndex = getCategoryIndex(category); if (categoryIndex < 0) { throw new IllegalArgumentException( "DefaultIntervalCategoryDataset.setValue: " + "unrecognised category."); } // update the data... this.startData[series][categoryIndex] = value; fireDatasetChanged(); } /** * Sets the end data value for one category in a series. * * @param series the series (zero-based index). * @param category the category. * * @param value the value. * * @see #setStartValue(int, Comparable, Number) */ public void setEndValue(int series, Comparable category, Number value) { // does the series exist? if ((series < 0) || (series > getSeriesCount() - 1)) { throw new IllegalArgumentException( "DefaultIntervalCategoryDataset.setValue: " + "series outside valid range."); } // is the category valid? int categoryIndex = getCategoryIndex(category); if (categoryIndex < 0) { throw new IllegalArgumentException( "DefaultIntervalCategoryDataset.setValue: " + "unrecognised category."); } // update the data... this.endData[series][categoryIndex] = value; fireDatasetChanged(); } /** * Returns the index for the given category. * * @param category the category (<code>null</code> not permitted). * * @return The index. * * @see #getColumnIndex(Comparable) */ public int getCategoryIndex(Comparable category) { int result = -1; for (int i = 0; i < this.categoryKeys.length; i++) { if (category.equals(this.categoryKeys[i])) { result = i; break; } } return result; } /** * Generates an array of keys, by appending a space plus an integer * (starting with 1) to the supplied prefix string. * * @param count the number of keys required. * @param prefix the name prefix. * * @return An array of <i>prefixN</i> with N = { 1 .. count}. */ private Comparable[] generateKeys(int count, String prefix) { Comparable[] result = new Comparable[count]; String name; for (int i = 0; i < count; i++) { name = prefix + (i + 1); result[i] = name; } return result; } /** * Returns a column key. * * @param column the column index. * * @return The column key. * * @see #getRowKey(int) */ @Override public Comparable getColumnKey(int column) { return this.categoryKeys[column]; } /** * Returns a column index. * * @param columnKey the column key (<code>null</code> not permitted). * * @return The column index. * * @see #getCategoryIndex(Comparable) */ @Override public int getColumnIndex(Comparable columnKey) { if (columnKey == null) { throw new IllegalArgumentException("Null 'columnKey' argument."); } return getCategoryIndex(columnKey); } /** * Returns a row index. * * @param rowKey the row key. * * @return The row index. * * @see #getSeriesIndex(Comparable) */ @Override public int getRowIndex(Comparable rowKey) { return getSeriesIndex(rowKey); } /** * Returns a list of the series in the dataset. This method supports the * {@link CategoryDataset} interface. * * @return A list of the series in the dataset. * * @see #getColumnKeys() */ @Override public List<Comparable> getRowKeys() { // the CategoryDataset interface expects a list of series, but // we've stored them in an array... if (this.seriesKeys == null) { return new java.util.ArrayList<Comparable>(); } else { return Collections.unmodifiableList(Arrays.asList(this.seriesKeys)); } } /** * Returns the name of the specified series. * * @param row the index of the required row/series (zero-based). * * @return The name of the specified series. * * @see #getColumnKey(int) */ @Override public Comparable getRowKey(int row) { if ((row >= getRowCount()) || (row < 0)) { throw new IllegalArgumentException( "The 'row' argument is out of bounds."); } return this.seriesKeys[row]; } /** * Returns the number of categories in the dataset. This method is part of * the {@link CategoryDataset} interface. * * @return The number of categories in the dataset. * * @see #getCategoryCount() * @see #getRowCount() */ @Override public int getColumnCount() { return this.categoryKeys.length; } /** * Returns the number of series in the dataset (possibly zero). * * @return The number of series in the dataset. * * @see #getSeriesCount() * @see #getColumnCount() */ @Override public int getRowCount() { return this.seriesKeys.length; } /** * Tests this dataset for equality with an arbitrary object. * * @param obj the object (<code>null</code> permitted). * * @return A boolean. */ @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (!(obj instanceof DefaultIntervalCategoryDataset)) { return false; } DefaultIntervalCategoryDataset that = (DefaultIntervalCategoryDataset) obj; if (!Arrays.equals(this.seriesKeys, that.seriesKeys)) { return false; } if (!Arrays.equals(this.categoryKeys, that.categoryKeys)) { return false; } if (!equal(this.startData, that.startData)) { return false; } if (!equal(this.endData, that.endData)) { return false; } // seem to be the same... return true; } /** * Returns a clone of this dataset. * * @return A clone. * * @throws CloneNotSupportedException if there is a problem cloning the * dataset. */ @Override public Object clone() throws CloneNotSupportedException { DefaultIntervalCategoryDataset clone = (DefaultIntervalCategoryDataset) super.clone(); clone.categoryKeys = this.categoryKeys.clone(); clone.seriesKeys = this.seriesKeys.clone(); clone.startData = clone(this.startData); clone.endData = clone(this.endData); return clone; } /** * Tests two double[][] arrays for equality. * * @param array1 the first array (<code>null</code> permitted). * @param array2 the second arrray (<code>null</code> permitted). * * @return A boolean. */ private static boolean equal(Number[][] array1, Number[][] array2) { if (array1 == null) { return (array2 == null); } if (array2 == null) { return false; } if (array1.length != array2.length) { return false; } for (int i = 0; i < array1.length; i++) { if (!Arrays.equals(array1[i], array2[i])) { return false; } } return true; } /** * Clones a two dimensional array of <code>Number</code> objects. * * @param array the array (<code>null</code> not permitted). * * @return A clone of the array. */ private static Number[][] clone(Number[][] array) { if (array == null) { throw new IllegalArgumentException("Null 'array' argument."); } Number[][] result = new Number[array.length][]; for (int i = 0; i < array.length; i++) { Number[] child = array[i]; Number[] copychild = new Number[child.length]; System.arraycopy(child, 0, copychild, 0, child.length); result[i] = copychild; } return result; } }