001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
006     *
007     * Project Info:  http://www.jfree.org/jfreechart/index.html
008     *
009     * This library is free software; you can redistribute it and/or modify it 
010     * under the terms of the GNU Lesser General Public License as published by 
011     * the Free Software Foundation; either version 2.1 of the License, or 
012     * (at your option) any later version.
013     *
014     * This library is distributed in the hope that it will be useful, but 
015     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
016     * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
017     * License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this library; if not, write to the Free Software
021     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
022     * USA.  
023     *
024     * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
025     * in the United States and other countries.]
026     *
027     * -----------------------------------
028     * DefaultIntervalCategoryDataset.java
029     * -----------------------------------
030     * (C) Copyright 2002-2007, by Jeremy Bowman and Contributors.
031     *
032     * Original Author:  Jeremy Bowman;
033     * Contributor(s):   David Gilbert (for Object Refinery Limited);
034     *
035     * $Id: DefaultIntervalCategoryDataset.java,v 1.9.2.3 2007/01/17 15:51:15 mungady Exp $
036     *
037     * Changes
038     * -------
039     * 29-Apr-2002 : Version 1, contributed by Jeremy Bowman (DG);
040     * 24-Oct-2002 : Amendments for changes made to the dataset interface (DG);
041     *
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.general.AbstractSeriesDataset;
054    
055    /**
056     * A convenience class that provides a default implementation of the
057     * {@link IntervalCategoryDataset} interface.
058     * <p>
059     * The standard constructor accepts data in a two dimensional array where the
060     * first dimension is the series, and the second dimension is the category.
061     */
062    public class DefaultIntervalCategoryDataset extends AbstractSeriesDataset
063                                                implements IntervalCategoryDataset {
064    
065        /** The series keys. */
066        private Comparable[] seriesKeys;
067    
068        /** The category keys. */
069        private Comparable[] categoryKeys;
070    
071        /** Storage for the start value data. */
072        private Number[][] startData;
073    
074        /** Storage for the end value data. */
075        private Number[][] endData;
076    
077        /**
078         * Creates a new dataset.
079         *
080         * @param starts  the starting values for the intervals.
081         * @param ends  the ending values for the intervals.
082         */
083        public DefaultIntervalCategoryDataset(double[][] starts, double[][] ends) {
084            this(DataUtilities.createNumberArray2D(starts),
085                    DataUtilities.createNumberArray2D(ends));
086        }
087    
088        /**
089         * Constructs a dataset and populates it with data from the array.
090         * <p>
091         * The arrays are indexed as data[series][category].  Series and category
092         * names are automatically generated - you can change them using the
093         * {@link #setSeriesKeys(Comparable[])} and 
094         * {@link #setCategoryKeys(Comparable[])} methods.
095         *
096         * @param starts  the start values data.
097         * @param ends  the end values data.
098         */
099        public DefaultIntervalCategoryDataset(Number[][] starts, Number[][] ends) {
100            this(null, null, starts, ends);
101        }
102    
103        /**
104         * Constructs a DefaultIntervalCategoryDataset, populates it with data
105         * from the arrays, and uses the supplied names for the series.
106         * <p>
107         * Category names are generated automatically ("Category 1", "Category 2",
108         * etc).
109         *
110         * @param seriesNames  the series names.
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.
128         * @param categoryKeys  the categories.
129         * @param starts  the start values data, indexed as data[series][category].
130         * @param ends  the end values data, indexed as data[series][category].
131         */
132        public DefaultIntervalCategoryDataset(Comparable[] seriesKeys,
133                                              Comparable[] categoryKeys,
134                                              Number[][] starts,
135                                              Number[][] ends) {
136    
137            this.startData = starts;
138            this.endData = ends;
139    
140            if (starts != null && ends != null) {
141    
142                String baseName = "org.jfree.data.resources.DataPackageResources";
143                ResourceBundle resources = ResourceBundle.getBundle(baseName);
144    
145                int seriesCount = starts.length;
146                if (seriesCount != ends.length) {
147                    String errMsg = "DefaultIntervalCategoryDataset: the number "
148                        + "of series in the start value dataset does "
149                        + "not match the number of series in the end "
150                        + "value dataset.";
151                    throw new IllegalArgumentException(errMsg);
152                }
153                if (seriesCount > 0) {
154    
155                    // set up the series names...
156                    if (seriesKeys != null) {
157    
158                        if (seriesKeys.length != seriesCount) {
159                            throw new IllegalArgumentException(
160                                    "The number of series keys does not "
161                                    + "match the number of series in the data.");
162                        }
163    
164                        this.seriesKeys = seriesKeys;
165                    }
166                    else {
167                        String prefix = resources.getString(
168                                "series.default-prefix") + " ";
169                        this.seriesKeys = generateKeys(seriesCount, prefix);
170                    }
171    
172                    // set up the category names...
173                    int categoryCount = starts[0].length;
174                    if (categoryCount != ends[0].length) {
175                        String errMsg = "DefaultIntervalCategoryDataset: the "
176                                    + "number of categories in the start value "
177                                    + "dataset does not match the number of "
178                                    + "categories in the end value dataset.";
179                        throw new IllegalArgumentException(errMsg);
180                    }
181                    if (categoryKeys != null) {
182                        if (categoryKeys.length != categoryCount) {
183                            throw new IllegalArgumentException(
184                                    "The number of category keys does not match "
185                                    + "the number of categories in the data.");
186                        }
187                        this.categoryKeys = categoryKeys;
188                    }
189                    else {
190                        String prefix = resources.getString(
191                                "categories.default-prefix") + " ";
192                        this.categoryKeys = generateKeys(categoryCount, prefix);
193                    }
194    
195                }
196                else {
197                    this.seriesKeys = null;
198                    this.categoryKeys = null;
199                }
200            }
201    
202        }
203    
204        /**
205         * Returns the number of series in the dataset (possibly zero).
206         *
207         * @return The number of series in the dataset.
208         */
209        public int getSeriesCount() {
210            int result = 0;
211            if (this.startData != null) {
212                result = this.startData.length;
213            }
214            return result;
215        }
216    
217        /**
218         * Returns the item count.
219         *
220         * @return The item count.
221         */
222        public int getItemCount() {
223            return this.categoryKeys.length;
224        }
225    
226        /**
227         * Returns a series index.
228         *
229         * @param series  the series key.
230         *
231         * @return The series index.
232         */
233        public int getSeriesIndex(Comparable series) {
234            List seriesKeys = getSeries();
235            return seriesKeys.indexOf(series);
236        }
237    
238        /**
239         * Returns the name of the specified series.
240         *
241         * @param series  the index of the required series (zero-based).
242         *
243         * @return The name of the specified series.
244         */
245        public Comparable getSeriesKey(int series) {
246            if ((series >= getSeriesCount()) || (series < 0)) {
247                throw new IllegalArgumentException("No such series : " + series);
248            }
249            return this.seriesKeys[series];
250        }
251    
252        /**
253         * Sets the names of the series in the dataset.
254         *
255         * @param seriesKeys  the keys of the series in the dataset.
256         */
257        public void setSeriesKeys(Comparable[] seriesKeys) {
258    
259            // check argument...
260            if (seriesKeys == null) {
261                throw new IllegalArgumentException("Null 'seriesKeys' argument.");
262            }
263    
264            if (seriesKeys.length != getSeriesCount()) {
265                throw new IllegalArgumentException(
266                        "DefaultIntervalCategoryDataset.setSeriesKeys(): "
267                        + "the number of series keys does not match the data.");
268            }
269    
270            // make the change...
271            this.seriesKeys = seriesKeys;
272            fireDatasetChanged();
273    
274        }
275    
276        /**
277         * Returns the number of categories in the dataset.
278         * <P>
279         * This method is part of the CategoryDataset interface.
280         *
281         * @return The number of categories in the dataset.
282         */
283        public int getCategoryCount() {
284            int result = 0;
285            if (this.startData != null) {
286                if (getSeriesCount() > 0) {
287                    result = this.startData[0].length;
288                }
289            }
290            return result;
291        }
292    
293        /**
294         * Returns a list of the series in the dataset.
295         * <P>
296         * Supports the CategoryDataset interface.
297         *
298         * @return A list of the series in the dataset.
299         */
300        public List getSeries() {
301    
302            // the CategoryDataset interface expects a list of series, but
303            // we've stored them in an array...
304            if (this.seriesKeys == null) {
305                return new java.util.ArrayList();
306            }
307            else {
308                return Collections.unmodifiableList(Arrays.asList(this.seriesKeys));
309            }
310    
311        }
312    
313        /**
314         * Returns a list of the categories in the dataset.
315         * <P>
316         * Supports the CategoryDataset interface.
317         *
318         * @return A list of the categories in the dataset.
319         */
320        public List getCategories() {
321            return getColumnKeys();
322        }
323    
324        /**
325         * Returns a list of the categories in the dataset.
326         * <P>
327         * Supports the CategoryDataset interface.
328         *
329         * @return A list of the categories in the dataset.
330         */
331        public List getColumnKeys() {
332    
333            // the CategoryDataset interface expects a list of categories, but
334            // we've stored them in an array...
335            if (this.categoryKeys == null) {
336                return new ArrayList();
337            }
338            else {
339                return Collections.unmodifiableList(Arrays.asList(
340                        this.categoryKeys));
341            }
342    
343        }
344    
345        /**
346         * Sets the categories for the dataset.
347         *
348         * @param categoryKeys  an array of objects representing the categories in 
349         *                      the dataset.
350         */
351        public void setCategoryKeys(Comparable[] categoryKeys) {
352    
353            // check arguments...
354            if (categoryKeys == null) {
355                throw new IllegalArgumentException("Null 'categoryKeys' argument.");
356            }
357    
358            if (categoryKeys.length != this.startData[0].length) {
359                throw new IllegalArgumentException(
360                        "The number of categories does not match the data.");
361            }
362    
363            for (int i = 0; i < categoryKeys.length; i++) {
364                if (categoryKeys[i] == null) {
365                    throw new IllegalArgumentException(
366                        "DefaultIntervalCategoryDataset.setCategoryKeys(): "
367                        + "null category not permitted.");
368                }
369            }
370    
371            // make the change...
372            this.categoryKeys = categoryKeys;
373            fireDatasetChanged();
374    
375        }
376    
377        /**
378         * Returns the data value for one category in a series.
379         * <P>
380         * This method is part of the CategoryDataset interface.  Not particularly
381         * meaningful for this class...returns the end value.
382         * @param series    The required series (zero based index).
383         * @param category  The required category.
384         * @return The data value for one category in a series (null possible).
385         */
386        public Number getValue(Comparable series, Comparable category) {
387            int seriesIndex = getSeriesIndex(series);
388            int itemIndex = getColumnIndex(category);
389            return getValue(seriesIndex, itemIndex);
390        }
391    
392        /**
393         * Returns the data value for one category in a series.
394         * <P>
395         * This method is part of the CategoryDataset interface.  Not particularly
396         * meaningful for this class...returns the end value.
397         *
398         * @param series  the required series (zero based index).
399         * @param category  the required category.
400         *
401         * @return The data value for one category in a series (null possible).
402         */
403        public Number getValue(int series, int category) {
404            return getEndValue(series, category);
405        }
406    
407        /**
408         * Returns the start data value for one category in a series.
409         *
410         * @param series  the required series.
411         * @param category  the required category.
412         *
413         * @return The start data value for one category in a series 
414         *         (possibly <code>null</code>).
415         */
416        public Number getStartValue(Comparable series, Comparable category) {
417            int seriesIndex = getSeriesIndex(series);
418            int itemIndex = getColumnIndex(category);
419            return getStartValue(seriesIndex, itemIndex);
420        }
421    
422        /**
423         * Returns the start data value for one category in a series.
424         *
425         * @param series  the required series (zero based index).
426         * @param category  the required category.
427         *
428         * @return The start data value for one category in a series 
429         *         (possibly <code>null</code>).
430         */
431        public Number getStartValue(int series, int category) {
432    
433            // check arguments...
434            if ((series < 0) || (series >= getSeriesCount())) {
435                throw new IllegalArgumentException(
436                    "DefaultIntervalCategoryDataset.getValue(): "
437                    + "series index out of range.");
438            }
439    
440            if ((category < 0) || (category >= getCategoryCount())) {
441                throw new IllegalArgumentException(
442                    "DefaultIntervalCategoryDataset.getValue(): "
443                    + "category index out of range.");
444            }
445    
446            // fetch the value...
447            return this.startData[series][category];
448    
449        }
450    
451        /**
452         * Returns the end data value for one category in a series.
453         *
454         * @param series  the required series.
455         * @param category  the required category.
456         *
457         * @return The end data value for one category in a series (null possible).
458         */
459        public Number getEndValue(Comparable series, Comparable category) {
460            int seriesIndex = getSeriesIndex(series);
461            int itemIndex = getColumnIndex(category);
462            return getEndValue(seriesIndex, itemIndex);
463        }
464    
465        /**
466         * Returns the end data value for one category in a series.
467         *
468         * @param series  the required series (zero based index).
469         * @param category  the required category.
470         *
471         * @return The end data value for one category in a series (null possible).
472         */
473        public Number getEndValue(int series, int category) {
474    
475            // check arguments...
476            if ((series < 0) || (series >= getSeriesCount())) {
477                throw new IllegalArgumentException(
478                    "DefaultIntervalCategoryDataset.getValue(): "
479                    + "series index out of range.");
480            }
481    
482            if ((category < 0) || (category >= getCategoryCount())) {
483                throw new IllegalArgumentException(
484                    "DefaultIntervalCategoryDataset.getValue(): "
485                    + "category index out of range.");
486            }
487    
488            // fetch the value...
489            return this.endData[series][category];
490    
491        }
492    
493        /**
494         * Sets the start data value for one category in a series.
495         * 
496         * @param series  the series (zero-based index).
497         * @param category  the category.
498         * 
499         * @param value The value.
500         */
501        public void setStartValue(int series, Comparable category, Number value) {
502    
503            // does the series exist?
504            if ((series < 0) || (series > getSeriesCount())) {
505                throw new IllegalArgumentException(
506                    "DefaultIntervalCategoryDataset.setValue: "
507                    + "series outside valid range.");
508            }
509    
510            // is the category valid?
511            int categoryIndex = getCategoryIndex(category);
512            if (categoryIndex < 0) {
513                throw new IllegalArgumentException(
514                    "DefaultIntervalCategoryDataset.setValue: "
515                    + "unrecognised category.");
516            }
517    
518            // update the data...
519            this.startData[series][categoryIndex] = value;
520            fireDatasetChanged();
521    
522        }
523    
524        /**
525         * Sets the end data value for one category in a series.
526         *
527         * @param series  the series (zero-based index).
528         * @param category  the category.
529         *
530         * @param value the value.
531         */
532        public void setEndValue(int series, Comparable category, Number value) {
533    
534            // does the series exist?
535            if ((series < 0) || (series > getSeriesCount())) {
536                throw new IllegalArgumentException(
537                    "DefaultIntervalCategoryDataset.setValue: "
538                    + "series outside valid range.");
539            }
540    
541            // is the category valid?
542            int categoryIndex = getCategoryIndex(category);
543            if (categoryIndex < 0) {
544                throw new IllegalArgumentException(
545                    "DefaultIntervalCategoryDataset.setValue: "
546                    + "unrecognised category.");
547            }
548    
549            // update the data...
550            this.endData[series][categoryIndex] = value;
551            fireDatasetChanged();
552    
553        }
554    
555        /**
556         * Returns the index for the given category.
557         *
558         * @param category  the category.
559         *
560         * @return The index.
561         */
562        private int getCategoryIndex(Comparable category) {
563            int result = -1;
564            for (int i = 0; i < this.categoryKeys.length; i++) {
565                if (category.equals(this.categoryKeys[i])) {
566                    result = i;
567                    break;
568                }
569            }
570            return result;
571        }
572    
573        /**
574         * Generates an array of keys, by appending a space plus an integer
575         * (starting with 1) to the supplied prefix string.
576         *
577         * @param count  the number of keys required.
578         * @param prefix  the name prefix.
579         *
580         * @return An array of <i>prefixN</i> with N = { 1 .. count}.
581         */
582        private Comparable[] generateKeys(int count, String prefix) {
583            Comparable[] result = new Comparable[count];
584            String name;
585            for (int i = 0; i < count; i++) {
586                name = prefix + (i + 1);
587                result[i] = name;
588            }
589            return result;
590        }
591    
592        /**
593         * Returns a column key.
594         *
595         * @param column  the column index.
596         *
597         * @return The column key.
598         */
599        public Comparable getColumnKey(int column) {
600            return this.categoryKeys[column];
601        }
602    
603        /**
604         * Returns a column index.
605         *
606         * @param columnKey  the column key.
607         *
608         * @return The column index.
609         */
610        public int getColumnIndex(Comparable columnKey) {
611            List categories = getCategories();
612            return categories.indexOf(columnKey);
613        }
614    
615        /**
616         * Returns a row index.
617         *
618         * @param rowKey  the row key.
619         *
620         * @return The row index.
621         */
622        public int getRowIndex(Comparable rowKey) {
623            List seriesKeys = getSeries();
624            return seriesKeys.indexOf(rowKey);
625        }
626    
627        /**
628         * Returns a list of the series in the dataset.
629         * <P>
630         * Supports the CategoryDataset interface.
631         *
632         * @return A list of the series in the dataset.
633         */
634        public List getRowKeys() {
635            // the CategoryDataset interface expects a list of series, but
636            // we've stored them in an array...
637            if (this.seriesKeys == null) {
638                return new java.util.ArrayList();
639            }
640            else {
641                return Collections.unmodifiableList(Arrays.asList(this.seriesKeys));
642            }
643        }
644    
645        /**
646         * Returns the name of the specified series.
647         *
648         * @param row  the index of the required row/series (zero-based).
649         *
650         * @return The name of the specified series.
651         */
652        public Comparable getRowKey(int row) {
653            if ((row >= getRowCount()) || (row < 0)) {
654                throw new IllegalArgumentException(
655                        "The 'row' argument is out of bounds.");
656            }
657            return this.seriesKeys[row];
658        }
659    
660        /**
661         * Returns the number of categories in the dataset.  This method is part of 
662         * the {@link CategoryDataset} interface.
663         *
664         * @return The number of categories in the dataset.
665         */
666        public int getColumnCount() {
667            int result = 0;
668            if (this.startData != null) {
669                if (getSeriesCount() > 0) {
670                    result = this.startData[0].length;
671                }
672            }
673            return result;
674        }
675    
676        /**
677         * Returns the number of series in the dataset (possibly zero).
678         *
679         * @return The number of series in the dataset.
680         */
681        public int getRowCount() {
682            int result = 0;
683            if (this.startData != null) {
684                result = this.startData.length;
685            }
686            return result;
687        }
688    
689    }