001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2008, 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     * DatasetUtilities.java
029     * ---------------------
030     * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors.
031     *
032     * Original Author:  David Gilbert (for Object Refinery Limited);
033     * Contributor(s):   Andrzej Porebski (bug fix);
034     *                   Jonathan Nash (bug fix);
035     *                   Richard Atkinson;
036     *                   Andreas Schroeder;
037     *                   Rafal Skalny (patch 1925366);
038     *                   Jerome David (patch 2131001);
039     *
040     * Changes (from 18-Sep-2001)
041     * --------------------------
042     * 18-Sep-2001 : Added standard header and fixed DOS encoding problem (DG);
043     * 22-Oct-2001 : Renamed DataSource.java --> Dataset.java etc. (DG);
044     * 15-Nov-2001 : Moved to package com.jrefinery.data.* in the JCommon class
045     *               library (DG);
046     *               Changed to handle null values from datasets (DG);
047     *               Bug fix (thanks to Andrzej Porebski) - initial value now set
048     *               to positive or negative infinity when iterating (DG);
049     * 22-Nov-2001 : Datasets with containing no data now return null for min and
050     *               max calculations (DG);
051     * 13-Dec-2001 : Extended to handle HighLowDataset and IntervalXYDataset (DG);
052     * 15-Feb-2002 : Added getMinimumStackedRangeValue() and
053     *               getMaximumStackedRangeValue() (DG);
054     * 28-Feb-2002 : Renamed Datasets.java --> DatasetUtilities.java (DG);
055     * 18-Mar-2002 : Fixed bug in min/max domain calculation for datasets that
056     *               implement the CategoryDataset interface AND the XYDataset
057     *               interface at the same time.  Thanks to Jonathan Nash for the
058     *               fix (DG);
059     * 23-Apr-2002 : Added getDomainExtent() and getRangeExtent() methods (DG);
060     * 13-Jun-2002 : Modified range measurements to handle
061     *               IntervalCategoryDataset (DG);
062     * 12-Jul-2002 : Method name change in DomainInfo interface (DG);
063     * 30-Jul-2002 : Added pie dataset summation method (DG);
064     * 01-Oct-2002 : Added a method for constructing an XYDataset from a Function2D
065     *               instance (DG);
066     * 24-Oct-2002 : Amendments required following changes to the CategoryDataset
067     *               interface (DG);
068     * 18-Nov-2002 : Changed CategoryDataset to TableDataset (DG);
069     * 04-Mar-2003 : Added isEmpty(XYDataset) method (DG);
070     * 05-Mar-2003 : Added a method for creating a CategoryDataset from a
071     *               KeyedValues instance (DG);
072     * 15-May-2003 : Renamed isEmpty --> isEmptyOrNull (DG);
073     * 25-Jun-2003 : Added limitPieDataset methods (RA);
074     * 26-Jun-2003 : Modified getDomainExtent() method to accept null datasets (DG);
075     * 27-Jul-2003 : Added getStackedRangeExtent(TableXYDataset data) (RA);
076     * 18-Aug-2003 : getStackedRangeExtent(TableXYDataset data) now handles null
077     *               values (RA);
078     * 02-Sep-2003 : Added method to check for null or empty PieDataset (DG);
079     * 18-Sep-2003 : Fix for bug 803660 (getMaximumRangeValue for
080     *               CategoryDataset) (DG);
081     * 20-Oct-2003 : Added getCumulativeRangeExtent() method (DG);
082     * 09-Jan-2003 : Added argument checking code to the createCategoryDataset()
083     *               method (DG);
084     * 23-Mar-2004 : Fixed bug in getMaximumStackedRangeValue() method (DG);
085     * 31-Mar-2004 : Exposed the extent iteration algorithms to use one of them and
086     *               applied noninstantiation pattern (AS);
087     * 11-May-2004 : Renamed getPieDatasetTotal --> calculatePieDatasetTotal (DG);
088     * 15-Jul-2004 : Switched getX() with getXValue() and getY() with getYValue();
089     * 24-Aug-2004 : Added argument checks to createCategoryDataset() method (DG);
090     * 04-Oct-2004 : Renamed ArrayUtils --> ArrayUtilities (DG);
091     * 06-Oct-2004 : Renamed findDomainExtent() --> findDomainBounds(),
092     *               findRangeExtent() --> findRangeBounds() (DG);
093     * 07-Jan-2005 : Renamed findStackedRangeExtent() --> findStackedRangeBounds(),
094     *               findCumulativeRangeExtent() --> findCumulativeRangeBounds(),
095     *               iterateXYRangeExtent() --> iterateXYRangeBounds(),
096     *               removed deprecated methods (DG);
097     * 03-Feb-2005 : The findStackedRangeBounds() methods now return null for
098     *               empty datasets (DG);
099     * 03-Mar-2005 : Moved createNumberArray() and createNumberArray2D() methods
100     *               from DatasetUtilities --> DataUtilities (DG);
101     * 22-Sep-2005 : Added new findStackedRangeBounds() method that takes base
102     *               argument (DG);
103     * ------------- JFREECHART 1.0.x ---------------------------------------------
104     * 15-Mar-2007 : Added calculateStackTotal() method (DG);
105     * 27-Mar-2008 : Fixed bug in findCumulativeRangeBounds() method (DG);
106     * 28-Mar-2008 : Fixed sample count in sampleFunction2D() method, renamed
107     *               iterateXYRangeBounds() --> iterateRangeBounds(XYDataset), and
108     *               fixed a bug in findRangeBounds(XYDataset, false) (DG);
109     * 28-Mar-2008 : Applied a variation of patch 1925366 (from Rafal Skalny) for
110     *               slightly more efficient iterateRangeBounds() methods (DG);
111     * 08-Apr-2008 : Fixed typo in iterateRangeBounds() (DG);
112     * 08-Oct-2008 : Applied patch 2131001 by Jerome David, with some modifications
113     *               and additions and some new unit tests (DG);
114     *
115     */
116    
117    package org.jfree.data.general;
118    
119    import java.util.ArrayList;
120    import java.util.Iterator;
121    import java.util.List;
122    
123    import org.jfree.data.DomainInfo;
124    import org.jfree.data.KeyToGroupMap;
125    import org.jfree.data.KeyedValues;
126    import org.jfree.data.Range;
127    import org.jfree.data.RangeInfo;
128    import org.jfree.data.category.CategoryDataset;
129    import org.jfree.data.category.DefaultCategoryDataset;
130    import org.jfree.data.category.IntervalCategoryDataset;
131    import org.jfree.data.function.Function2D;
132    import org.jfree.data.xy.IntervalXYDataset;
133    import org.jfree.data.xy.OHLCDataset;
134    import org.jfree.data.xy.TableXYDataset;
135    import org.jfree.data.xy.XYDataset;
136    import org.jfree.data.xy.XYSeries;
137    import org.jfree.data.xy.XYSeriesCollection;
138    import org.jfree.util.ArrayUtilities;
139    
140    /**
141     * A collection of useful static methods relating to datasets.
142     */
143    public final class DatasetUtilities {
144    
145        /**
146         * Private constructor for non-instanceability.
147         */
148        private DatasetUtilities() {
149            // now try to instantiate this ;-)
150        }
151    
152        /**
153         * Calculates the total of all the values in a {@link PieDataset}.  If
154         * the dataset contains negative or <code>null</code> values, they are
155         * ignored.
156         *
157         * @param dataset  the dataset (<code>null</code> not permitted).
158         *
159         * @return The total.
160         */
161        public static double calculatePieDatasetTotal(PieDataset dataset) {
162            if (dataset == null) {
163                throw new IllegalArgumentException("Null 'dataset' argument.");
164            }
165            List keys = dataset.getKeys();
166            double totalValue = 0;
167            Iterator iterator = keys.iterator();
168            while (iterator.hasNext()) {
169                Comparable current = (Comparable) iterator.next();
170                if (current != null) {
171                    Number value = dataset.getValue(current);
172                    double v = 0.0;
173                    if (value != null) {
174                        v = value.doubleValue();
175                    }
176                    if (v > 0) {
177                        totalValue = totalValue + v;
178                    }
179                }
180            }
181            return totalValue;
182        }
183    
184        /**
185         * Creates a pie dataset from a table dataset by taking all the values
186         * for a single row.
187         *
188         * @param dataset  the dataset (<code>null</code> not permitted).
189         * @param rowKey  the row key.
190         *
191         * @return A pie dataset.
192         */
193        public static PieDataset createPieDatasetForRow(CategoryDataset dataset,
194                                                        Comparable rowKey) {
195            int row = dataset.getRowIndex(rowKey);
196            return createPieDatasetForRow(dataset, row);
197        }
198    
199        /**
200         * Creates a pie dataset from a table dataset by taking all the values
201         * for a single row.
202         *
203         * @param dataset  the dataset (<code>null</code> not permitted).
204         * @param row  the row (zero-based index).
205         *
206         * @return A pie dataset.
207         */
208        public static PieDataset createPieDatasetForRow(CategoryDataset dataset,
209                                                        int row) {
210            DefaultPieDataset result = new DefaultPieDataset();
211            int columnCount = dataset.getColumnCount();
212            for (int current = 0; current < columnCount; current++) {
213                Comparable columnKey = dataset.getColumnKey(current);
214                result.setValue(columnKey, dataset.getValue(row, current));
215            }
216            return result;
217        }
218    
219        /**
220         * Creates a pie dataset from a table dataset by taking all the values
221         * for a single column.
222         *
223         * @param dataset  the dataset (<code>null</code> not permitted).
224         * @param columnKey  the column key.
225         *
226         * @return A pie dataset.
227         */
228        public static PieDataset createPieDatasetForColumn(CategoryDataset dataset,
229                                                           Comparable columnKey) {
230            int column = dataset.getColumnIndex(columnKey);
231            return createPieDatasetForColumn(dataset, column);
232        }
233    
234        /**
235         * Creates a pie dataset from a {@link CategoryDataset} by taking all the
236         * values for a single column.
237         *
238         * @param dataset  the dataset (<code>null</code> not permitted).
239         * @param column  the column (zero-based index).
240         *
241         * @return A pie dataset.
242         */
243        public static PieDataset createPieDatasetForColumn(CategoryDataset dataset,
244                                                           int column) {
245            DefaultPieDataset result = new DefaultPieDataset();
246            int rowCount = dataset.getRowCount();
247            for (int i = 0; i < rowCount; i++) {
248                Comparable rowKey = dataset.getRowKey(i);
249                result.setValue(rowKey, dataset.getValue(i, column));
250            }
251            return result;
252        }
253    
254        /**
255         * Creates a new pie dataset based on the supplied dataset, but modified
256         * by aggregating all the low value items (those whose value is lower
257         * than the <code>percentThreshold</code>) into a single item with the
258         * key "Other".
259         *
260         * @param source  the source dataset (<code>null</code> not permitted).
261         * @param key  a new key for the aggregated items (<code>null</code> not
262         *             permitted).
263         * @param minimumPercent  the percent threshold.
264         *
265         * @return The pie dataset with (possibly) aggregated items.
266         */
267        public static PieDataset createConsolidatedPieDataset(PieDataset source,
268                                                              Comparable key,
269                                                              double minimumPercent)
270        {
271            return DatasetUtilities.createConsolidatedPieDataset(
272                source, key, minimumPercent, 2
273            );
274        }
275    
276        /**
277         * Creates a new pie dataset based on the supplied dataset, but modified
278         * by aggregating all the low value items (those whose value is lower
279         * than the <code>percentThreshold</code>) into a single item.  The
280         * aggregated items are assigned the specified key.  Aggregation only
281         * occurs if there are at least <code>minItems</code> items to aggregate.
282         *
283         * @param source  the source dataset (<code>null</code> not permitted).
284         * @param key  the key to represent the aggregated items.
285         * @param minimumPercent  the percent threshold (ten percent is 0.10).
286         * @param minItems  only aggregate low values if there are at least this
287         *                  many.
288         *
289         * @return The pie dataset with (possibly) aggregated items.
290         */
291        public static PieDataset createConsolidatedPieDataset(PieDataset source,
292                                                              Comparable key,
293                                                              double minimumPercent,
294                                                              int minItems) {
295    
296            DefaultPieDataset result = new DefaultPieDataset();
297            double total = DatasetUtilities.calculatePieDatasetTotal(source);
298    
299            //  Iterate and find all keys below threshold percentThreshold
300            List keys = source.getKeys();
301            ArrayList otherKeys = new ArrayList();
302            Iterator iterator = keys.iterator();
303            while (iterator.hasNext()) {
304                Comparable currentKey = (Comparable) iterator.next();
305                Number dataValue = source.getValue(currentKey);
306                if (dataValue != null) {
307                    double value = dataValue.doubleValue();
308                    if (value / total < minimumPercent) {
309                        otherKeys.add(currentKey);
310                    }
311                }
312            }
313    
314            //  Create new dataset with keys above threshold percentThreshold
315            iterator = keys.iterator();
316            double otherValue = 0;
317            while (iterator.hasNext()) {
318                Comparable currentKey = (Comparable) iterator.next();
319                Number dataValue = source.getValue(currentKey);
320                if (dataValue != null) {
321                    if (otherKeys.contains(currentKey)
322                        && otherKeys.size() >= minItems) {
323                        //  Do not add key to dataset
324                        otherValue += dataValue.doubleValue();
325                    }
326                    else {
327                        //  Add key to dataset
328                        result.setValue(currentKey, dataValue);
329                    }
330                }
331            }
332            //  Add other category if applicable
333            if (otherKeys.size() >= minItems) {
334                result.setValue(key, otherValue);
335            }
336            return result;
337        }
338    
339        /**
340         * Creates a {@link CategoryDataset} that contains a copy of the data in an
341         * array (instances of <code>Double</code> are created to represent the
342         * data items).
343         * <p>
344         * Row and column keys are created by appending 0, 1, 2, ... to the
345         * supplied prefixes.
346         *
347         * @param rowKeyPrefix  the row key prefix.
348         * @param columnKeyPrefix  the column key prefix.
349         * @param data  the data.
350         *
351         * @return The dataset.
352         */
353        public static CategoryDataset createCategoryDataset(String rowKeyPrefix,
354                                                            String columnKeyPrefix,
355                                                            double[][] data) {
356    
357            DefaultCategoryDataset result = new DefaultCategoryDataset();
358            for (int r = 0; r < data.length; r++) {
359                String rowKey = rowKeyPrefix + (r + 1);
360                for (int c = 0; c < data[r].length; c++) {
361                    String columnKey = columnKeyPrefix + (c + 1);
362                    result.addValue(new Double(data[r][c]), rowKey, columnKey);
363                }
364            }
365            return result;
366    
367        }
368    
369        /**
370         * Creates a {@link CategoryDataset} that contains a copy of the data in
371         * an array.
372         * <p>
373         * Row and column keys are created by appending 0, 1, 2, ... to the
374         * supplied prefixes.
375         *
376         * @param rowKeyPrefix  the row key prefix.
377         * @param columnKeyPrefix  the column key prefix.
378         * @param data  the data.
379         *
380         * @return The dataset.
381         */
382        public static CategoryDataset createCategoryDataset(String rowKeyPrefix,
383                                                            String columnKeyPrefix,
384                                                            Number[][] data) {
385    
386            DefaultCategoryDataset result = new DefaultCategoryDataset();
387            for (int r = 0; r < data.length; r++) {
388                String rowKey = rowKeyPrefix + (r + 1);
389                for (int c = 0; c < data[r].length; c++) {
390                    String columnKey = columnKeyPrefix + (c + 1);
391                    result.addValue(data[r][c], rowKey, columnKey);
392                }
393            }
394            return result;
395    
396        }
397    
398        /**
399         * Creates a {@link CategoryDataset} that contains a copy of the data in
400         * an array (instances of <code>Double</code> are created to represent the
401         * data items).
402         * <p>
403         * Row and column keys are taken from the supplied arrays.
404         *
405         * @param rowKeys  the row keys (<code>null</code> not permitted).
406         * @param columnKeys  the column keys (<code>null</code> not permitted).
407         * @param data  the data.
408         *
409         * @return The dataset.
410         */
411        public static CategoryDataset createCategoryDataset(Comparable[] rowKeys,
412                                                            Comparable[] columnKeys,
413                                                            double[][] data) {
414    
415            // check arguments...
416            if (rowKeys == null) {
417                throw new IllegalArgumentException("Null 'rowKeys' argument.");
418            }
419            if (columnKeys == null) {
420                throw new IllegalArgumentException("Null 'columnKeys' argument.");
421            }
422            if (ArrayUtilities.hasDuplicateItems(rowKeys)) {
423                throw new IllegalArgumentException("Duplicate items in 'rowKeys'.");
424            }
425            if (ArrayUtilities.hasDuplicateItems(columnKeys)) {
426                throw new IllegalArgumentException(
427                    "Duplicate items in 'columnKeys'."
428                );
429            }
430            if (rowKeys.length != data.length) {
431                throw new IllegalArgumentException(
432                    "The number of row keys does not match the number of rows in "
433                    + "the data array."
434                );
435            }
436            int columnCount = 0;
437            for (int r = 0; r < data.length; r++) {
438                columnCount = Math.max(columnCount, data[r].length);
439            }
440            if (columnKeys.length != columnCount) {
441                throw new IllegalArgumentException(
442                    "The number of column keys does not match the number of "
443                    + "columns in the data array."
444                );
445            }
446    
447            // now do the work...
448            DefaultCategoryDataset result = new DefaultCategoryDataset();
449            for (int r = 0; r < data.length; r++) {
450                Comparable rowKey = rowKeys[r];
451                for (int c = 0; c < data[r].length; c++) {
452                    Comparable columnKey = columnKeys[c];
453                    result.addValue(new Double(data[r][c]), rowKey, columnKey);
454                }
455            }
456            return result;
457    
458        }
459    
460        /**
461         * Creates a {@link CategoryDataset} by copying the data from the supplied
462         * {@link KeyedValues} instance.
463         *
464         * @param rowKey  the row key (<code>null</code> not permitted).
465         * @param rowData  the row data (<code>null</code> not permitted).
466         *
467         * @return A dataset.
468         */
469        public static CategoryDataset createCategoryDataset(Comparable rowKey,
470                                                            KeyedValues rowData) {
471    
472            if (rowKey == null) {
473                throw new IllegalArgumentException("Null 'rowKey' argument.");
474            }
475            if (rowData == null) {
476                throw new IllegalArgumentException("Null 'rowData' argument.");
477            }
478            DefaultCategoryDataset result = new DefaultCategoryDataset();
479            for (int i = 0; i < rowData.getItemCount(); i++) {
480                result.addValue(rowData.getValue(i), rowKey, rowData.getKey(i));
481            }
482            return result;
483    
484        }
485    
486        /**
487         * Creates an {@link XYDataset} by sampling the specified function over a
488         * fixed range.
489         *
490         * @param f  the function (<code>null</code> not permitted).
491         * @param start  the start value for the range.
492         * @param end  the end value for the range.
493         * @param samples  the number of sample points (must be > 1).
494         * @param seriesKey  the key to give the resulting series
495         *                   (<code>null</code> not permitted).
496         *
497         * @return A dataset.
498         */
499        public static XYDataset sampleFunction2D(Function2D f, double start,
500                double end, int samples, Comparable seriesKey) {
501    
502            if (f == null) {
503                throw new IllegalArgumentException("Null 'f' argument.");
504            }
505            if (seriesKey == null) {
506                throw new IllegalArgumentException("Null 'seriesKey' argument.");
507            }
508            if (start >= end) {
509                throw new IllegalArgumentException("Requires 'start' < 'end'.");
510            }
511            if (samples < 2) {
512                throw new IllegalArgumentException("Requires 'samples' > 1");
513            }
514    
515            XYSeries series = new XYSeries(seriesKey);
516            double step = (end - start) / (samples - 1);
517            for (int i = 0; i < samples; i++) {
518                double x = start + (step * i);
519                series.add(x, f.getValue(x));
520            }
521            XYSeriesCollection collection = new XYSeriesCollection(series);
522            return collection;
523        }
524    
525        /**
526         * Returns <code>true</code> if the dataset is empty (or <code>null</code>),
527         * and <code>false</code> otherwise.
528         *
529         * @param dataset  the dataset (<code>null</code> permitted).
530         *
531         * @return A boolean.
532         */
533        public static boolean isEmptyOrNull(PieDataset dataset) {
534    
535            if (dataset == null) {
536                return true;
537            }
538    
539            int itemCount = dataset.getItemCount();
540            if (itemCount == 0) {
541                return true;
542            }
543    
544            for (int item = 0; item < itemCount; item++) {
545                Number y = dataset.getValue(item);
546                if (y != null) {
547                    double yy = y.doubleValue();
548                    if (yy > 0.0) {
549                        return false;
550                    }
551                }
552            }
553    
554            return true;
555    
556        }
557    
558        /**
559         * Returns <code>true</code> if the dataset is empty (or <code>null</code>),
560         * and <code>false</code> otherwise.
561         *
562         * @param dataset  the dataset (<code>null</code> permitted).
563         *
564         * @return A boolean.
565         */
566        public static boolean isEmptyOrNull(CategoryDataset dataset) {
567    
568            if (dataset == null) {
569                return true;
570            }
571    
572            int rowCount = dataset.getRowCount();
573            int columnCount = dataset.getColumnCount();
574            if (rowCount == 0 || columnCount == 0) {
575                return true;
576            }
577    
578            for (int r = 0; r < rowCount; r++) {
579                for (int c = 0; c < columnCount; c++) {
580                    if (dataset.getValue(r, c) != null) {
581                        return false;
582                    }
583    
584                }
585            }
586    
587            return true;
588    
589        }
590    
591        /**
592         * Returns <code>true</code> if the dataset is empty (or <code>null</code>),
593         * and <code>false</code> otherwise.
594         *
595         * @param dataset  the dataset (<code>null</code> permitted).
596         *
597         * @return A boolean.
598         */
599        public static boolean isEmptyOrNull(XYDataset dataset) {
600            if (dataset != null) {
601                for (int s = 0; s < dataset.getSeriesCount(); s++) {
602                    if (dataset.getItemCount(s) > 0) {
603                        return false;
604                    }
605                }
606            }
607            return true;
608        }
609    
610        /**
611         * Returns the range of values in the domain (x-values) of a dataset.
612         *
613         * @param dataset  the dataset (<code>null</code> not permitted).
614         *
615         * @return The range of values (possibly <code>null</code>).
616         */
617        public static Range findDomainBounds(XYDataset dataset) {
618            return findDomainBounds(dataset, true);
619        }
620    
621        /**
622         * Returns the range of values in the domain (x-values) of a dataset.
623         *
624         * @param dataset  the dataset (<code>null</code> not permitted).
625         * @param includeInterval  determines whether or not the x-interval is taken
626         *                         into account (only applies if the dataset is an
627         *                         {@link IntervalXYDataset}).
628         *
629         * @return The range of values (possibly <code>null</code>).
630         */
631        public static Range findDomainBounds(XYDataset dataset,
632                                             boolean includeInterval) {
633    
634            if (dataset == null) {
635                throw new IllegalArgumentException("Null 'dataset' argument.");
636            }
637    
638            Range result = null;
639            // if the dataset implements DomainInfo, life is easier
640            if (dataset instanceof DomainInfo) {
641                DomainInfo info = (DomainInfo) dataset;
642                result = info.getDomainBounds(includeInterval);
643            }
644            else {
645                result = iterateDomainBounds(dataset, includeInterval);
646            }
647            return result;
648    
649        }
650    
651        /**
652         * Iterates over the items in an {@link XYDataset} to find
653         * the range of x-values.  If the dataset is an instance of
654         * {@link IntervalXYDataset}, the starting and ending x-values
655         * will be used for the bounds calculation.
656         *
657         * @param dataset  the dataset (<code>null</code> not permitted).
658         *
659         * @return The range (possibly <code>null</code>).
660         */
661        public static Range iterateDomainBounds(XYDataset dataset) {
662            return iterateDomainBounds(dataset, true);
663        }
664    
665        /**
666         * Iterates over the items in an {@link XYDataset} to find
667         * the range of x-values.
668         *
669         * @param dataset  the dataset (<code>null</code> not permitted).
670         * @param includeInterval  a flag that determines, for an
671         *          {@link IntervalXYDataset}, whether the x-interval or just the
672         *          x-value is used to determine the overall range.
673         *
674         * @return The range (possibly <code>null</code>).
675         */
676        public static Range iterateDomainBounds(XYDataset dataset,
677                                                boolean includeInterval) {
678            if (dataset == null) {
679                throw new IllegalArgumentException("Null 'dataset' argument.");
680            }
681            double minimum = Double.POSITIVE_INFINITY;
682            double maximum = Double.NEGATIVE_INFINITY;
683            int seriesCount = dataset.getSeriesCount();
684            double lvalue;
685            double uvalue;
686            if (includeInterval && dataset instanceof IntervalXYDataset) {
687                IntervalXYDataset intervalXYData = (IntervalXYDataset) dataset;
688                for (int series = 0; series < seriesCount; series++) {
689                    int itemCount = dataset.getItemCount(series);
690                    for (int item = 0; item < itemCount; item++) {
691                        lvalue = intervalXYData.getStartXValue(series, item);
692                        uvalue = intervalXYData.getEndXValue(series, item);
693                        if (!Double.isNaN(lvalue)) {
694                            minimum = Math.min(minimum, lvalue);
695                        }
696                        if (!Double.isNaN(uvalue)) {
697                            maximum = Math.max(maximum, uvalue);
698                        }
699                    }
700                }
701            }
702            else {
703                for (int series = 0; series < seriesCount; series++) {
704                    int itemCount = dataset.getItemCount(series);
705                    for (int item = 0; item < itemCount; item++) {
706                        lvalue = dataset.getXValue(series, item);
707                        uvalue = lvalue;
708                        if (!Double.isNaN(lvalue)) {
709                            minimum = Math.min(minimum, lvalue);
710                            maximum = Math.max(maximum, uvalue);
711                        }
712                    }
713                }
714            }
715            if (minimum > maximum) {
716                return null;
717            }
718            else {
719                return new Range(minimum, maximum);
720            }
721        }
722    
723        /**
724         * Returns the range of values in the range for the dataset.
725         *
726         * @param dataset  the dataset (<code>null</code> not permitted).
727         *
728         * @return The range (possibly <code>null</code>).
729         */
730        public static Range findRangeBounds(CategoryDataset dataset) {
731            return findRangeBounds(dataset, true);
732        }
733    
734        /**
735         * Returns the range of values in the range for the dataset.
736         *
737         * @param dataset  the dataset (<code>null</code> not permitted).
738         * @param includeInterval  a flag that determines whether or not the
739         *                         y-interval is taken into account.
740         *
741         * @return The range (possibly <code>null</code>).
742         */
743        public static Range findRangeBounds(CategoryDataset dataset,
744                                            boolean includeInterval) {
745            if (dataset == null) {
746                throw new IllegalArgumentException("Null 'dataset' argument.");
747            }
748            Range result = null;
749            if (dataset instanceof RangeInfo) {
750                RangeInfo info = (RangeInfo) dataset;
751                result = info.getRangeBounds(includeInterval);
752            }
753            else {
754                result = iterateRangeBounds(dataset, includeInterval);
755            }
756            return result;
757        }
758    
759        /**
760         * Returns the range of values in the range for the dataset.  This method
761         * is the partner for the {@link #findDomainBounds(XYDataset)} method.
762         *
763         * @param dataset  the dataset (<code>null</code> not permitted).
764         *
765         * @return The range (possibly <code>null</code>).
766         */
767        public static Range findRangeBounds(XYDataset dataset) {
768            return findRangeBounds(dataset, true);
769        }
770    
771        /**
772         * Returns the range of values in the range for the dataset.  This method
773         * is the partner for the {@link #findDomainBounds(XYDataset, boolean)}
774         * method.
775         *
776         * @param dataset  the dataset (<code>null</code> not permitted).
777         * @param includeInterval  a flag that determines whether or not the
778         *                         y-interval is taken into account.
779         *
780         * @return The range (possibly <code>null</code>).
781         */
782        public static Range findRangeBounds(XYDataset dataset,
783                                            boolean includeInterval) {
784            if (dataset == null) {
785                throw new IllegalArgumentException("Null 'dataset' argument.");
786            }
787            Range result = null;
788            if (dataset instanceof RangeInfo) {
789                RangeInfo info = (RangeInfo) dataset;
790                result = info.getRangeBounds(includeInterval);
791            }
792            else {
793                result = iterateRangeBounds(dataset, includeInterval);
794            }
795            return result;
796        }
797    
798        /**
799         * Iterates over the data item of the category dataset to find
800         * the range bounds.
801         *
802         * @param dataset  the dataset (<code>null</code> not permitted).
803         * @param includeInterval  a flag that determines whether or not the
804         *                         y-interval is taken into account.
805         *
806         * @return The range (possibly <code>null</code>).
807         *
808         * @deprecated As of 1.0.10, use
809         *         {@link #iterateRangeBounds(CategoryDataset, boolean)}.
810         */
811        public static Range iterateCategoryRangeBounds(CategoryDataset dataset,
812                boolean includeInterval) {
813            return iterateRangeBounds(dataset, includeInterval);
814        }
815    
816        /**
817         * Iterates over the data item of the category dataset to find
818         * the range bounds.
819         *
820         * @param dataset  the dataset (<code>null</code> not permitted).
821         *
822         * @return The range (possibly <code>null</code>).
823         *
824         * @since 1.0.10
825         */
826        public static Range iterateRangeBounds(CategoryDataset dataset) {
827            return iterateRangeBounds(dataset, true);
828        }
829    
830        /**
831         * Iterates over the data item of the category dataset to find
832         * the range bounds.
833         *
834         * @param dataset  the dataset (<code>null</code> not permitted).
835         * @param includeInterval  a flag that determines whether or not the
836         *                         y-interval is taken into account.
837         *
838         * @return The range (possibly <code>null</code>).
839         *
840         * @since 1.0.10
841         */
842        public static Range iterateRangeBounds(CategoryDataset dataset,
843                boolean includeInterval) {
844            double minimum = Double.POSITIVE_INFINITY;
845            double maximum = Double.NEGATIVE_INFINITY;
846            int rowCount = dataset.getRowCount();
847            int columnCount = dataset.getColumnCount();
848            if (includeInterval && dataset instanceof IntervalCategoryDataset) {
849                // handle the special case where the dataset has y-intervals that
850                // we want to measure
851                IntervalCategoryDataset icd = (IntervalCategoryDataset) dataset;
852                Number lvalue, uvalue;
853                for (int row = 0; row < rowCount; row++) {
854                    for (int column = 0; column < columnCount; column++) {
855                        lvalue = icd.getStartValue(row, column);
856                        uvalue = icd.getEndValue(row, column);
857                        if (lvalue != null && !Double.isNaN(lvalue.doubleValue())) {
858                            minimum = Math.min(minimum, lvalue.doubleValue());
859                        }
860                        if (uvalue != null && !Double.isNaN(uvalue.doubleValue())) {
861                            maximum = Math.max(maximum, uvalue.doubleValue());
862                        }
863                    }
864                }
865            }
866            else {
867                // handle the standard case (plain CategoryDataset)
868                for (int row = 0; row < rowCount; row++) {
869                    for (int column = 0; column < columnCount; column++) {
870                        Number value = dataset.getValue(row, column);
871                        if (value != null) {
872                            double v = value.doubleValue();
873                            if (!Double.isNaN(v)) {
874                                minimum = Math.min(minimum, v);
875                                maximum = Math.max(maximum, v);
876                            }
877                        }
878                    }
879                }
880            }
881            if (minimum == Double.POSITIVE_INFINITY) {
882                return null;
883            }
884            else {
885                return new Range(minimum, maximum);
886            }
887        }
888    
889        /**
890         * Iterates over the data item of the xy dataset to find
891         * the range bounds.
892         *
893         * @param dataset  the dataset (<code>null</code> not permitted).
894         *
895         * @return The range (possibly <code>null</code>).
896         *
897         * @deprecated As of 1.0.10, use {@link #iterateRangeBounds(XYDataset)}.
898         */
899        public static Range iterateXYRangeBounds(XYDataset dataset) {
900            return iterateRangeBounds(dataset);
901        }
902    
903        /**
904         * Iterates over the data item of the xy dataset to find
905         * the range bounds.
906         *
907         * @param dataset  the dataset (<code>null</code> not permitted).
908         *
909         * @return The range (possibly <code>null</code>).
910         *
911         * @since 1.0.10
912         */
913        public static Range iterateRangeBounds(XYDataset dataset) {
914            return iterateRangeBounds(dataset, true);
915        }
916    
917        /**
918         * Iterates over the data items of the xy dataset to find
919         * the range bounds.
920         *
921         * @param dataset  the dataset (<code>null</code> not permitted).
922         * @param includeInterval  a flag that determines, for an
923         *          {@link IntervalXYDataset}, whether the y-interval or just the
924         *          y-value is used to determine the overall range.
925         *
926         * @return The range (possibly <code>null</code>).
927         *
928         * @since 1.0.10
929         */
930        public static Range iterateRangeBounds(XYDataset dataset,
931                boolean includeInterval) {
932            double minimum = Double.POSITIVE_INFINITY;
933            double maximum = Double.NEGATIVE_INFINITY;
934            int seriesCount = dataset.getSeriesCount();
935    
936            // handle three cases by dataset type
937            if (includeInterval && dataset instanceof IntervalXYDataset) {
938                // handle special case of IntervalXYDataset
939                IntervalXYDataset ixyd = (IntervalXYDataset) dataset;
940                for (int series = 0; series < seriesCount; series++) {
941                    int itemCount = dataset.getItemCount(series);
942                    for (int item = 0; item < itemCount; item++) {
943                        double lvalue = ixyd.getStartYValue(series, item);
944                        double uvalue = ixyd.getEndYValue(series, item);
945                        if (!Double.isNaN(lvalue)) {
946                            minimum = Math.min(minimum, lvalue);
947                        }
948                        if (!Double.isNaN(uvalue)) {
949                            maximum = Math.max(maximum, uvalue);
950                        }
951                    }
952                }
953            }
954            else if (includeInterval && dataset instanceof OHLCDataset) {
955                // handle special case of OHLCDataset
956                OHLCDataset ohlc = (OHLCDataset) dataset;
957                for (int series = 0; series < seriesCount; series++) {
958                    int itemCount = dataset.getItemCount(series);
959                    for (int item = 0; item < itemCount; item++) {
960                        double lvalue = ohlc.getLowValue(series, item);
961                        double uvalue = ohlc.getHighValue(series, item);
962                        if (!Double.isNaN(lvalue)) {
963                            minimum = Math.min(minimum, lvalue);
964                        }
965                        if (!Double.isNaN(uvalue)) {
966                            maximum = Math.max(maximum, uvalue);
967                        }
968                    }
969                }
970            }
971            else {
972                // standard case - plain XYDataset
973                for (int series = 0; series < seriesCount; series++) {
974                    int itemCount = dataset.getItemCount(series);
975                    for (int item = 0; item < itemCount; item++) {
976                        double value = dataset.getYValue(series, item);
977                        if (!Double.isNaN(value)) {
978                            minimum = Math.min(minimum, value);
979                            maximum = Math.max(maximum, value);
980                        }
981                    }
982                }
983            }
984            if (minimum == Double.POSITIVE_INFINITY) {
985                return null;
986            }
987            else {
988                return new Range(minimum, maximum);
989            }
990        }
991    
992        /**
993         * Finds the minimum domain (or X) value for the specified dataset.  This
994         * is easy if the dataset implements the {@link DomainInfo} interface (a
995         * good idea if there is an efficient way to determine the minimum value).
996         * Otherwise, it involves iterating over the entire data-set.
997         * <p>
998         * Returns <code>null</code> if all the data values in the dataset are
999         * <code>null</code>.
1000         *
1001         * @param dataset  the dataset (<code>null</code> not permitted).
1002         *
1003         * @return The minimum value (possibly <code>null</code>).
1004         */
1005        public static Number findMinimumDomainValue(XYDataset dataset) {
1006            if (dataset == null) {
1007                throw new IllegalArgumentException("Null 'dataset' argument.");
1008            }
1009            Number result = null;
1010            // if the dataset implements DomainInfo, life is easy
1011            if (dataset instanceof DomainInfo) {
1012                DomainInfo info = (DomainInfo) dataset;
1013                return new Double(info.getDomainLowerBound(true));
1014            }
1015            else {
1016                double minimum = Double.POSITIVE_INFINITY;
1017                int seriesCount = dataset.getSeriesCount();
1018                for (int series = 0; series < seriesCount; series++) {
1019                    int itemCount = dataset.getItemCount(series);
1020                    for (int item = 0; item < itemCount; item++) {
1021    
1022                        double value;
1023                        if (dataset instanceof IntervalXYDataset) {
1024                            IntervalXYDataset intervalXYData
1025                                = (IntervalXYDataset) dataset;
1026                            value = intervalXYData.getStartXValue(series, item);
1027                        }
1028                        else {
1029                            value = dataset.getXValue(series, item);
1030                        }
1031                        if (!Double.isNaN(value)) {
1032                            minimum = Math.min(minimum, value);
1033                        }
1034    
1035                    }
1036                }
1037                if (minimum == Double.POSITIVE_INFINITY) {
1038                    result = null;
1039                }
1040                else {
1041                    result = new Double(minimum);
1042                }
1043            }
1044    
1045            return result;
1046        }
1047    
1048        /**
1049         * Returns the maximum domain value for the specified dataset.  This is
1050         * easy if the dataset implements the {@link DomainInfo} interface (a good
1051         * idea if there is an efficient way to determine the maximum value).
1052         * Otherwise, it involves iterating over the entire data-set.  Returns
1053         * <code>null</code> if all the data values in the dataset are
1054         * <code>null</code>.
1055         *
1056         * @param dataset  the dataset (<code>null</code> not permitted).
1057         *
1058         * @return The maximum value (possibly <code>null</code>).
1059         */
1060        public static Number findMaximumDomainValue(XYDataset dataset) {
1061            if (dataset == null) {
1062                throw new IllegalArgumentException("Null 'dataset' argument.");
1063            }
1064            Number result = null;
1065            // if the dataset implements DomainInfo, life is easy
1066            if (dataset instanceof DomainInfo) {
1067                DomainInfo info = (DomainInfo) dataset;
1068                return new Double(info.getDomainUpperBound(true));
1069            }
1070    
1071            // hasn't implemented DomainInfo, so iterate...
1072            else {
1073                double maximum = Double.NEGATIVE_INFINITY;
1074                int seriesCount = dataset.getSeriesCount();
1075                for (int series = 0; series < seriesCount; series++) {
1076                    int itemCount = dataset.getItemCount(series);
1077                    for (int item = 0; item < itemCount; item++) {
1078    
1079                        double value;
1080                        if (dataset instanceof IntervalXYDataset) {
1081                            IntervalXYDataset intervalXYData
1082                                = (IntervalXYDataset) dataset;
1083                            value = intervalXYData.getEndXValue(series, item);
1084                        }
1085                        else {
1086                            value = dataset.getXValue(series, item);
1087                        }
1088                        if (!Double.isNaN(value)) {
1089                            maximum = Math.max(maximum, value);
1090                        }
1091                    }
1092                }
1093                if (maximum == Double.NEGATIVE_INFINITY) {
1094                    result = null;
1095                }
1096                else {
1097                    result = new Double(maximum);
1098                }
1099    
1100            }
1101    
1102            return result;
1103        }
1104    
1105        /**
1106         * Returns the minimum range value for the specified dataset.  This is
1107         * easy if the dataset implements the {@link RangeInfo} interface (a good
1108         * idea if there is an efficient way to determine the minimum value).
1109         * Otherwise, it involves iterating over the entire data-set.  Returns
1110         * <code>null</code> if all the data values in the dataset are
1111         * <code>null</code>.
1112         *
1113         * @param dataset  the dataset (<code>null</code> not permitted).
1114         *
1115         * @return The minimum value (possibly <code>null</code>).
1116         */
1117        public static Number findMinimumRangeValue(CategoryDataset dataset) {
1118    
1119            // check parameters...
1120            if (dataset == null) {
1121                throw new IllegalArgumentException("Null 'dataset' argument.");
1122            }
1123    
1124            // work out the minimum value...
1125            if (dataset instanceof RangeInfo) {
1126                RangeInfo info = (RangeInfo) dataset;
1127                return new Double(info.getRangeLowerBound(true));
1128            }
1129    
1130            // hasn't implemented RangeInfo, so we'll have to iterate...
1131            else {
1132                double minimum = Double.POSITIVE_INFINITY;
1133                int seriesCount = dataset.getRowCount();
1134                int itemCount = dataset.getColumnCount();
1135                for (int series = 0; series < seriesCount; series++) {
1136                    for (int item = 0; item < itemCount; item++) {
1137                        Number value;
1138                        if (dataset instanceof IntervalCategoryDataset) {
1139                            IntervalCategoryDataset icd
1140                                = (IntervalCategoryDataset) dataset;
1141                            value = icd.getStartValue(series, item);
1142                        }
1143                        else {
1144                            value = dataset.getValue(series, item);
1145                        }
1146                        if (value != null) {
1147                            minimum = Math.min(minimum, value.doubleValue());
1148                        }
1149                    }
1150                }
1151                if (minimum == Double.POSITIVE_INFINITY) {
1152                    return null;
1153                }
1154                else {
1155                    return new Double(minimum);
1156                }
1157    
1158            }
1159    
1160        }
1161    
1162        /**
1163         * Returns the minimum range value for the specified dataset.  This is
1164         * easy if the dataset implements the {@link RangeInfo} interface (a good
1165         * idea if there is an efficient way to determine the minimum value).
1166         * Otherwise, it involves iterating over the entire data-set.  Returns
1167         * <code>null</code> if all the data values in the dataset are
1168         * <code>null</code>.
1169         *
1170         * @param dataset  the dataset (<code>null</code> not permitted).
1171         *
1172         * @return The minimum value (possibly <code>null</code>).
1173         */
1174        public static Number findMinimumRangeValue(XYDataset dataset) {
1175    
1176            if (dataset == null) {
1177                throw new IllegalArgumentException("Null 'dataset' argument.");
1178            }
1179    
1180            // work out the minimum value...
1181            if (dataset instanceof RangeInfo) {
1182                RangeInfo info = (RangeInfo) dataset;
1183                return new Double(info.getRangeLowerBound(true));
1184            }
1185    
1186            // hasn't implemented RangeInfo, so we'll have to iterate...
1187            else {
1188                double minimum = Double.POSITIVE_INFINITY;
1189                int seriesCount = dataset.getSeriesCount();
1190                for (int series = 0; series < seriesCount; series++) {
1191                    int itemCount = dataset.getItemCount(series);
1192                    for (int item = 0; item < itemCount; item++) {
1193    
1194                        double value;
1195                        if (dataset instanceof IntervalXYDataset) {
1196                            IntervalXYDataset intervalXYData
1197                                = (IntervalXYDataset) dataset;
1198                            value = intervalXYData.getStartYValue(series, item);
1199                        }
1200                        else if (dataset instanceof OHLCDataset) {
1201                            OHLCDataset highLowData = (OHLCDataset) dataset;
1202                            value = highLowData.getLowValue(series, item);
1203                        }
1204                        else {
1205                            value = dataset.getYValue(series, item);
1206                        }
1207                        if (!Double.isNaN(value)) {
1208                            minimum = Math.min(minimum, value);
1209                        }
1210    
1211                    }
1212                }
1213                if (minimum == Double.POSITIVE_INFINITY) {
1214                    return null;
1215                }
1216                else {
1217                    return new Double(minimum);
1218                }
1219    
1220            }
1221    
1222        }
1223    
1224        /**
1225         * Returns the maximum range value for the specified dataset.  This is easy
1226         * if the dataset implements the {@link RangeInfo} interface (a good idea
1227         * if there is an efficient way to determine the maximum value).
1228         * Otherwise, it involves iterating over the entire data-set.  Returns
1229         * <code>null</code> if all the data values are <code>null</code>.
1230         *
1231         * @param dataset  the dataset (<code>null</code> not permitted).
1232         *
1233         * @return The maximum value (possibly <code>null</code>).
1234         */
1235        public static Number findMaximumRangeValue(CategoryDataset dataset) {
1236    
1237            if (dataset == null) {
1238                throw new IllegalArgumentException("Null 'dataset' argument.");
1239            }
1240    
1241            // work out the minimum value...
1242            if (dataset instanceof RangeInfo) {
1243                RangeInfo info = (RangeInfo) dataset;
1244                return new Double(info.getRangeUpperBound(true));
1245            }
1246    
1247            // hasn't implemented RangeInfo, so we'll have to iterate...
1248            else {
1249    
1250                double maximum = Double.NEGATIVE_INFINITY;
1251                int seriesCount = dataset.getRowCount();
1252                int itemCount = dataset.getColumnCount();
1253                for (int series = 0; series < seriesCount; series++) {
1254                    for (int item = 0; item < itemCount; item++) {
1255                        Number value;
1256                        if (dataset instanceof IntervalCategoryDataset) {
1257                            IntervalCategoryDataset icd
1258                                = (IntervalCategoryDataset) dataset;
1259                            value = icd.getEndValue(series, item);
1260                        }
1261                        else {
1262                            value = dataset.getValue(series, item);
1263                        }
1264                        if (value != null) {
1265                            maximum = Math.max(maximum, value.doubleValue());
1266                        }
1267                    }
1268                }
1269                if (maximum == Double.NEGATIVE_INFINITY) {
1270                    return null;
1271                }
1272                else {
1273                    return new Double(maximum);
1274                }
1275    
1276            }
1277    
1278        }
1279    
1280        /**
1281         * Returns the maximum range value for the specified dataset.  This is
1282         * easy if the dataset implements the {@link RangeInfo} interface (a good
1283         * idea if there is an efficient way to determine the maximum value).
1284         * Otherwise, it involves iterating over the entire data-set.  Returns
1285         * <code>null</code> if all the data values are <code>null</code>.
1286         *
1287         * @param dataset  the dataset (<code>null</code> not permitted).
1288         *
1289         * @return The maximum value (possibly <code>null</code>).
1290         */
1291        public static Number findMaximumRangeValue(XYDataset dataset) {
1292    
1293            if (dataset == null) {
1294                throw new IllegalArgumentException("Null 'dataset' argument.");
1295            }
1296    
1297            // work out the minimum value...
1298            if (dataset instanceof RangeInfo) {
1299                RangeInfo info = (RangeInfo) dataset;
1300                return new Double(info.getRangeUpperBound(true));
1301            }
1302    
1303            // hasn't implemented RangeInfo, so we'll have to iterate...
1304            else  {
1305    
1306                double maximum = Double.NEGATIVE_INFINITY;
1307                int seriesCount = dataset.getSeriesCount();
1308                for (int series = 0; series < seriesCount; series++) {
1309                    int itemCount = dataset.getItemCount(series);
1310                    for (int item = 0; item < itemCount; item++) {
1311                        double value;
1312                        if (dataset instanceof IntervalXYDataset) {
1313                            IntervalXYDataset intervalXYData
1314                                = (IntervalXYDataset) dataset;
1315                            value = intervalXYData.getEndYValue(series, item);
1316                        }
1317                        else if (dataset instanceof OHLCDataset) {
1318                            OHLCDataset highLowData = (OHLCDataset) dataset;
1319                            value = highLowData.getHighValue(series, item);
1320                        }
1321                        else {
1322                            value = dataset.getYValue(series, item);
1323                        }
1324                        if (!Double.isNaN(value)) {
1325                            maximum = Math.max(maximum, value);
1326                        }
1327                    }
1328                }
1329                if (maximum == Double.NEGATIVE_INFINITY) {
1330                    return null;
1331                }
1332                else {
1333                    return new Double(maximum);
1334                }
1335    
1336            }
1337    
1338        }
1339    
1340        /**
1341         * Returns the minimum and maximum values for the dataset's range
1342         * (y-values), assuming that the series in one category are stacked.
1343         *
1344         * @param dataset  the dataset (<code>null</code> not permitted).
1345         *
1346         * @return The range (<code>null</code> if the dataset contains no values).
1347         */
1348        publ