    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     * DatasetUtilities.java
    029     * ---------------------
    030     * (C) Copyright 2000-2007, 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 (beatification)
    037     *
    038     * $Id: DatasetUtilities.java,v 1.18.2.5 2007/03/15 17:04:35 mungady Exp $
    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     *
    106     */
    107
    108    package org.jfree.data.general;
    109
    110    import java.util.ArrayList;
    111    import java.util.Iterator;
    112    import java.util.List;
    113
    114    import org.jfree.data.DomainInfo;
    115    import org.jfree.data.KeyToGroupMap;
    116    import org.jfree.data.KeyedValues;
    117    import org.jfree.data.Range;
    118    import org.jfree.data.RangeInfo;
    119    import org.jfree.data.category.CategoryDataset;
    120    import org.jfree.data.category.DefaultCategoryDataset;
    121    import org.jfree.data.category.IntervalCategoryDataset;
    122    import org.jfree.data.function.Function2D;
    123    import org.jfree.data.xy.OHLCDataset;
    124    import org.jfree.data.xy.IntervalXYDataset;
    125    import org.jfree.data.xy.TableXYDataset;
    126    import org.jfree.data.xy.XYDataset;
    127    import org.jfree.data.xy.XYSeries;
    128    import org.jfree.data.xy.XYSeriesCollection;
    129    import org.jfree.util.ArrayUtilities;
    130
    131    /**
    132     * A collection of useful static methods relating to datasets.
    133     */
    134    public final class DatasetUtilities {
    135
    136        /**
    137         * Private constructor for non-instanceability.
    138         */
    139        private DatasetUtilities() {
    140            // now try to instantiate this ;-)
    141        }
    142
    143        /**
    144         * Calculates the total of all the values in a {@link PieDataset}.  If
    145         * the dataset contains negative or <code>null</code> values, they are
    146         * ignored.
    147         *
    148         * @param dataset  the dataset (<code>null</code> not permitted).
    149         *
    150         * @return The total.
    151         */
    152        public static double calculatePieDatasetTotal(PieDataset dataset) {
    153            if (dataset == null) {
    154                throw new IllegalArgumentException("Null 'dataset' argument.");
    155            }
    156            List keys = dataset.getKeys();
    157            double totalValue = 0;
    158            Iterator iterator = keys.iterator();
    159            while (iterator.hasNext()) {
    160                Comparable current = (Comparable) iterator.next();
    161                if (current != null) {
    162                    Number value = dataset.getValue(current);
    163                    double v = 0.0;
    164                    if (value != null) {
    165                        v = value.doubleValue();
    166                    }
    167                    if (v > 0) {
    168                        totalValue = totalValue + v;
    169                    }
    170                }
    171            }
    172            return totalValue;
    173        }
    174
    175        /**
    176         * Creates a pie dataset from a table dataset by taking all the values
    177         * for a single row.
    178         *
    179         * @param dataset  the dataset (<code>null</code> not permitted).
    180         * @param rowKey  the row key.
    181         *
    182         * @return A pie dataset.
    183         */
    184        public static PieDataset createPieDatasetForRow(CategoryDataset dataset,
    185                                                        Comparable rowKey) {
    186            int row = dataset.getRowIndex(rowKey);
    187            return createPieDatasetForRow(dataset, row);
    188        }
    189
    190        /**
    191         * Creates a pie dataset from a table dataset by taking all the values
    192         * for a single row.
    193         *
    194         * @param dataset  the dataset (<code>null</code> not permitted).
    195         * @param row  the row (zero-based index).
    196         *
    197         * @return A pie dataset.
    198         */
    199        public static PieDataset createPieDatasetForRow(CategoryDataset dataset,
    200                                                        int row) {
    201            DefaultPieDataset result = new DefaultPieDataset();
    202            int columnCount = dataset.getColumnCount();
    203            for (int current = 0; current < columnCount; current++) {
    204                Comparable columnKey = dataset.getColumnKey(current);
    205                result.setValue(columnKey, dataset.getValue(row, current));
    206            }
    207            return result;
    208        }
    209
    210        /**
    211         * Creates a pie dataset from a table dataset by taking all the values
    212         * for a single column.
    213         *
    214         * @param dataset  the dataset (<code>null</code> not permitted).
    215         * @param columnKey  the column key.
    216         *
    217         * @return A pie dataset.
    218         */
    219        public static PieDataset createPieDatasetForColumn(CategoryDataset dataset,
    220                                                           Comparable columnKey) {
    221            int column = dataset.getColumnIndex(columnKey);
    222            return createPieDatasetForColumn(dataset, column);
    223        }
    224
    225        /**
    226         * Creates a pie dataset from a {@link CategoryDataset} by taking all the
    227         * values for a single column.
    228         *
    229         * @param dataset  the dataset (<code>null</code> not permitted).
    230         * @param column  the column (zero-based index).
    231         *
    232         * @return A pie dataset.
    233         */
    234        public static PieDataset createPieDatasetForColumn(CategoryDataset dataset,
    235                                                           int column) {
    236            DefaultPieDataset result = new DefaultPieDataset();
    237            int rowCount = dataset.getRowCount();
    238            for (int i = 0; i < rowCount; i++) {
    239                Comparable rowKey = dataset.getRowKey(i);
    240                result.setValue(rowKey, dataset.getValue(i, column));
    241            }
    242            return result;
    243        }
    244
    245        /**
    246         * Creates a new pie dataset based on the supplied dataset, but modified
    247         * by aggregating all the low value items (those whose value is lower
    248         * than the <code>percentThreshold</code>) into a single item with the
    249         * key "Other".
    250         *
    251         * @param source  the source dataset (<code>null</code> not permitted).
    252         * @param key  a new key for the aggregated items (<code>null</code> not
    253         *             permitted).
    254         * @param minimumPercent  the percent threshold.
    255         *
    256         * @return The pie dataset with (possibly) aggregated items.
    257         */
    258        public static PieDataset createConsolidatedPieDataset(PieDataset source,
    259                                                              Comparable key,
    260                                                              double minimumPercent)
    261        {
    262            return DatasetUtilities.createConsolidatedPieDataset(
    263                source, key, minimumPercent, 2
    264            );
    265        }
    266
    267        /**
    268         * Creates a new pie dataset based on the supplied dataset, but modified
    269         * by aggregating all the low value items (those whose value is lower
    270         * than the <code>percentThreshold</code>) into a single item.  The
    271         * aggregated items are assigned the specified key.  Aggregation only
    272         * occurs if there are at least <code>minItems</code> items to aggregate.
    273         *
    274         * @param source  the source dataset (<code>null</code> not permitted).
    275         * @param key  the key to represent the aggregated items.
    276         * @param minimumPercent  the percent threshold (ten percent is 0.10).
    277         * @param minItems  only aggregate low values if there are at least this
    278         *                  many.
    279         *
    280         * @return The pie dataset with (possibly) aggregated items.
    281         */
    282        public static PieDataset createConsolidatedPieDataset(PieDataset source,
    283                                                              Comparable key,
    284                                                              double minimumPercent,
    285                                                              int minItems) {
    286
    287            DefaultPieDataset result = new DefaultPieDataset();
    288            double total = DatasetUtilities.calculatePieDatasetTotal(source);
    289
    290            //  Iterate and find all keys below threshold percentThreshold
    291            List keys = source.getKeys();
    292            ArrayList otherKeys = new ArrayList();
    293            Iterator iterator = keys.iterator();
    294            while (iterator.hasNext()) {
    295                Comparable currentKey = (Comparable) iterator.next();
    296                Number dataValue = source.getValue(currentKey);
    297                if (dataValue != null) {
    298                    double value = dataValue.doubleValue();
    299                    if (value / total < minimumPercent) {
    300                        otherKeys.add(currentKey);
    301                    }
    302                }
    303            }
    304
    305            //  Create new dataset with keys above threshold percentThreshold
    306            iterator = keys.iterator();
    307            double otherValue = 0;
    308            while (iterator.hasNext()) {
    309                Comparable currentKey = (Comparable) iterator.next();
    310                Number dataValue = source.getValue(currentKey);
    311                if (dataValue != null) {
    312                    if (otherKeys.contains(currentKey)
    313                        && otherKeys.size() >= minItems) {
    314                        //  Do not add key to dataset
    315                        otherValue += dataValue.doubleValue();
    316                    }
    317                    else {
    318                        //  Add key to dataset
    319                        result.setValue(currentKey, dataValue);
    320                    }
    321                }
    322            }
    323            //  Add other category if applicable
    324            if (otherKeys.size() >= minItems) {
    325                result.setValue(key, otherValue);
    326            }
    327            return result;
    328        }
    329
    330        /**
    331         * Creates a {@link CategoryDataset} that contains a copy of the data in an
    332         * array (instances of <code>Double</code> are created to represent the
    333         * data items).
    334         * <p>
    335         * Row and column keys are created by appending 0, 1, 2, ... to the
    336         * supplied prefixes.
    337         *
    338         * @param rowKeyPrefix  the row key prefix.
    339         * @param columnKeyPrefix  the column key prefix.
    340         * @param data  the data.
    341         *
    342         * @return The dataset.
    343         */
    344        public static CategoryDataset createCategoryDataset(String rowKeyPrefix,
    345                                                            String columnKeyPrefix,
    346                                                            double[][] data) {
    347
    348            DefaultCategoryDataset result = new DefaultCategoryDataset();
    349            for (int r = 0; r < data.length; r++) {
    350                String rowKey = rowKeyPrefix + (r + 1);
    351                for (int c = 0; c < data[r].length; c++) {
    352                    String columnKey = columnKeyPrefix + (c + 1);
    353                    result.addValue(new Double(data[r][c]), rowKey, columnKey);
    354                }
    355            }
    356            return result;
    357
    358        }
    359
    360        /**
    361         * Creates a {@link CategoryDataset} that contains a copy of the data in
    362         * an array.
    363         * <p>
    364         * Row and column keys are created by appending 0, 1, 2, ... to the
    365         * supplied prefixes.
    366         *
    367         * @param rowKeyPrefix  the row key prefix.
    368         * @param columnKeyPrefix  the column key prefix.
    369         * @param data  the data.
    370         *
    371         * @return The dataset.
    372         */
    373        public static CategoryDataset createCategoryDataset(String rowKeyPrefix,
    374                                                            String columnKeyPrefix,
    375                                                            Number[][] data) {
    376
    377            DefaultCategoryDataset result = new DefaultCategoryDataset();
    378            for (int r = 0; r < data.length; r++) {
    379                String rowKey = rowKeyPrefix + (r + 1);
    380                for (int c = 0; c < data[r].length; c++) {
    381                    String columnKey = columnKeyPrefix + (c + 1);
    382                    result.addValue(data[r][c], rowKey, columnKey);
    383                }
    384            }
    385            return result;
    386
    387        }
    388
    389        /**
    390         * Creates a {@link CategoryDataset} that contains a copy of the data in
    391         * an array (instances of <code>Double</code> are created to represent the
    392         * data items).
    393         * <p>
    394         * Row and column keys are taken from the supplied arrays.
    395         *
    396         * @param rowKeys  the row keys (<code>null</code> not permitted).
    397         * @param columnKeys  the column keys (<code>null</code> not permitted).
    398         * @param data  the data.
    399         *
    400         * @return The dataset.
    401         */
    402        public static CategoryDataset createCategoryDataset(Comparable[] rowKeys,
    403                                                            Comparable[] columnKeys,
    404                                                            double[][] data) {
    405
    406            // check arguments...
    407            if (rowKeys == null) {
    408                throw new IllegalArgumentException("Null 'rowKeys' argument.");
    409            }
    410            if (columnKeys == null) {
    411                throw new IllegalArgumentException("Null 'columnKeys' argument.");
    412            }
    413            if (ArrayUtilities.hasDuplicateItems(rowKeys)) {
    414                throw new IllegalArgumentException("Duplicate items in 'rowKeys'.");
    415            }
    416            if (ArrayUtilities.hasDuplicateItems(columnKeys)) {
    417                throw new IllegalArgumentException(
    418                    "Duplicate items in 'columnKeys'."
    419                );
    420            }
    421            if (rowKeys.length != data.length) {
    422                throw new IllegalArgumentException(
    423                    "The number of row keys does not match the number of rows in "
    424                    + "the data array."
    425                );
    426            }
    427            int columnCount = 0;
    428            for (int r = 0; r < data.length; r++) {
    429                columnCount = Math.max(columnCount, data[r].length);
    430            }
    431            if (columnKeys.length != columnCount) {
    432                throw new IllegalArgumentException(
    433                    "The number of column keys does not match the number of "
    434                    + "columns in the data array."
    435                );
    436            }
    437
    438            // now do the work...
    439            DefaultCategoryDataset result = new DefaultCategoryDataset();
    440            for (int r = 0; r < data.length; r++) {
    441                Comparable rowKey = rowKeys[r];
    442                for (int c = 0; c < data[r].length; c++) {
    443                    Comparable columnKey = columnKeys[c];
    444                    result.addValue(new Double(data[r][c]), rowKey, columnKey);
    445                }
    446            }
    447            return result;
    448
    449        }
    450
    451        /**
    452         * Creates a {@link CategoryDataset} by copying the data from the supplied
    453         * {@link KeyedValues} instance.
    454         *
    455         * @param rowKey  the row key (<code>null</code> not permitted).
    456         * @param rowData  the row data (<code>null</code> not permitted).
    457         *
    458         * @return A dataset.
    459         */
    460        public static CategoryDataset createCategoryDataset(Comparable rowKey,
    461                                                            KeyedValues rowData) {
    462
    463            if (rowKey == null) {
    464                throw new IllegalArgumentException("Null 'rowKey' argument.");
    465            }
    466            if (rowData == null) {
    467                throw new IllegalArgumentException("Null 'rowData' argument.");
    468            }
    469            DefaultCategoryDataset result = new DefaultCategoryDataset();
    470            for (int i = 0; i < rowData.getItemCount(); i++) {
    471                result.addValue(rowData.getValue(i), rowKey, rowData.getKey(i));
    472            }
    473            return result;
    474
    475        }
    476
    477        /**
    478         * Creates an {@link XYDataset} by sampling the specified function over a
    479         * fixed range.
    480         *
    481         * @param f  the function (<code>null</code> not permitted).
    482         * @param start  the start value for the range.
    483         * @param end  the end value for the range.
    484         * @param samples  the number of sample points (must be > 1).
    485         * @param seriesKey  the key to give the resulting series
    486         *                   (<code>null</code> not permitted).
    487         *
    488         * @return A dataset.
    489         */
    490        public static XYDataset sampleFunction2D(Function2D f,
    491                                                 double start,
    492                                                 double end,
    493                                                 int samples,
    494                                                 Comparable seriesKey) {
    495
    496            if (f == null) {
    497                throw new IllegalArgumentException("Null 'f' argument.");
    498            }
    499            if (seriesKey == null) {
    500                throw new IllegalArgumentException("Null 'seriesKey' argument.");
    501            }
    502            if (start >= end) {
    503                throw new IllegalArgumentException("Requires 'start' < 'end'.");
    504            }
    505            if (samples < 2) {
    506                throw new IllegalArgumentException("Requires 'samples' > 1");
    507            }
    508
    509            XYSeries series = new XYSeries(seriesKey);
    510            double step = (end - start) / samples;
    511            for (int i = 0; i <= samples; i++) {
    512                double x = start + (step * i);
    513                series.add(x, f.getValue(x));
    514            }
    515            XYSeriesCollection collection = new XYSeriesCollection(series);
    516            return collection;
    517
    518        }
    519
    520        /**
    521         * Returns <code>true</code> if the dataset is empty (or <code>null</code>),
    522         * and <code>false</code> otherwise.
    523         *
    524         * @param dataset  the dataset (<code>null</code> permitted).
    525         *
    526         * @return A boolean.
    527         */
    528        public static boolean isEmptyOrNull(PieDataset dataset) {
    529
    530            if (dataset == null) {
    531                return true;
    532            }
    533
    534            int itemCount = dataset.getItemCount();
    535            if (itemCount == 0) {
    536                return true;
    537            }
    538
    539            for (int item = 0; item < itemCount; item++) {
    540                Number y = dataset.getValue(item);
    541                if (y != null) {
    542                    double yy = y.doubleValue();
    543                    if (yy > 0.0) {
    544                        return false;
    545                    }
    546                }
    547            }
    548
    549            return true;
    550
    551        }
    552
    553        /**
    554         * Returns <code>true</code> if the dataset is empty (or <code>null</code>),
    555         * and <code>false</code> otherwise.
    556         *
    557         * @param dataset  the dataset (<code>null</code> permitted).
    558         *
    559         * @return A boolean.
    560         */
    561        public static boolean isEmptyOrNull(CategoryDataset dataset) {
    562
    563            if (dataset == null) {
    564                return true;
    565            }
    566
    567            int rowCount = dataset.getRowCount();
    568            int columnCount = dataset.getColumnCount();
    569            if (rowCount == 0 || columnCount == 0) {
    570                return true;
    571            }
    572
    573            for (int r = 0; r < rowCount; r++) {
    574                for (int c = 0; c < columnCount; c++) {
    575                    if (dataset.getValue(r, c) != null) {
    576                        return false;
    577                    }
    578
    579                }
    580            }
    581
    582            return true;
    583
    584        }
    585
    586        /**
    587         * Returns <code>true</code> if the dataset is empty (or <code>null</code>),
    588         * and <code>false</code> otherwise.
    589         *
    590         * @param dataset  the dataset (<code>null</code> permitted).
    591         *
    592         * @return A boolean.
    593         */
    594        public static boolean isEmptyOrNull(XYDataset dataset) {
    595
    596            boolean result = true;
    597
    598            if (dataset != null) {
    599                for (int s = 0; s < dataset.getSeriesCount(); s++) {
    600                    if (dataset.getItemCount(s) > 0) {
    601                        result = false;
    602                        continue;
    603                    }
    604                }
    605            }
    606
    607            return result;
    608
    609        }
    610
    611        /**
    612         * Returns the range of values in the domain (x-values) of a dataset.
    613         *
    614         * @param dataset  the dataset (<code>null</code> not permitted).
    615         *
    616         * @return The range of values (possibly <code>null</code>).
    617         */
    618        public static Range findDomainBounds(XYDataset dataset) {
    619            return findDomainBounds(dataset, true);
    620        }
    621
    622        /**
    623         * Returns the range of values in the domain (x-values) of a dataset.
    624         *
    625         * @param dataset  the dataset (<code>null</code> not permitted).
    626         * @param includeInterval  determines whether or not the x-interval is taken
    627         *                         into account (only applies if the dataset is an
    628         *                         {@link IntervalXYDataset}).
    629         *
    630         * @return The range of values (possibly <code>null</code>).
    631         */
    632        public static Range findDomainBounds(XYDataset dataset,
    633                                             boolean includeInterval) {
    634
    635            if (dataset == null) {
    636                throw new IllegalArgumentException("Null 'dataset' argument.");
    637            }
    638
    639            Range result = null;
    640            // if the dataset implements DomainInfo, life is easier
    641            if (dataset instanceof DomainInfo) {
    642                DomainInfo info = (DomainInfo) dataset;
    643                result = info.getDomainBounds(includeInterval);
    644            }
    645            else {
    646                result = iterateDomainBounds(dataset, includeInterval);
    647            }
    648            return result;
    649
    650        }
    651
    652        /**
    653         * Iterates over the items in an {@link XYDataset} to find
    654         * the range of x-values.
    655         *
    656         * @param dataset  the dataset (<code>null</code> not permitted).
    657         *
    658         * @return The range (possibly <code>null</code>).
    659         */
    660        public static Range iterateDomainBounds(XYDataset dataset) {
    661            return iterateDomainBounds(dataset, true);
    662        }
    663
    664        /**
    665         * Iterates over the items in an {@link XYDataset} to find
    666         * the range of x-values.
    667         *
    668         * @param dataset  the dataset (<code>null</code> not permitted).
    669         * @param includeInterval  a flag that determines, for an IntervalXYDataset,
    670         *                         whether the x-interval or just the x-value is
    671         *                         used to determine the overall range.
    672         *
    673         * @return The range (possibly <code>null</code>).
    674         */
    675        public static Range iterateDomainBounds(XYDataset dataset,
    676                                                boolean includeInterval) {
    677            if (dataset == null) {
    678                throw new IllegalArgumentException("Null 'dataset' argument.");
    679            }
    680            double minimum = Double.POSITIVE_INFINITY;
    681            double maximum = Double.NEGATIVE_INFINITY;
    682            int seriesCount = dataset.getSeriesCount();
    683            double lvalue;
    684            double uvalue;
    685            if (includeInterval && dataset instanceof IntervalXYDataset) {
    686                IntervalXYDataset intervalXYData = (IntervalXYDataset) dataset;
    687                for (int series = 0; series < seriesCount; series++) {
    688                    int itemCount = dataset.getItemCount(series);
    689                    for (int item = 0; item < itemCount; item++) {
    690                        lvalue = intervalXYData.getStartXValue(series, item);
    691                        uvalue = intervalXYData.getEndXValue(series, item);
    692                        minimum = Math.min(minimum, lvalue);
    693                        maximum = Math.max(maximum, uvalue);
    694                    }
    695                }
    696            }
    697            else {
    698                for (int series = 0; series < seriesCount; series++) {
    699                    int itemCount = dataset.getItemCount(series);
    700                    for (int item = 0; item < itemCount; item++) {
    701                        lvalue = dataset.getXValue(series, item);
    702                        uvalue = lvalue;
    703                        minimum = Math.min(minimum, lvalue);
    704                        maximum = Math.max(maximum, uvalue);
    705                    }
    706                }
    707            }
    708            if (minimum > maximum) {
    709                return null;
    710            }
    711            else {
    712                return new Range(minimum, maximum);
    713            }
    714        }
    715
    716        /**
    717         * Returns the range of values in the range for the dataset.  This method
    718         * is the partner for the getDomainExtent method.
    719         *
    720         * @param dataset  the dataset (<code>null</code> not permitted).
    721         *
    722         * @return The range (possibly <code>null</code>).
    723         */
    724        public static Range findRangeBounds(CategoryDataset dataset) {
    725            return findRangeBounds(dataset, true);
    726        }
    727
    728        /**
    729         * Returns the range of values in the range for the dataset.  This method
    730         * is the partner for the getDomainExtent method.
    731         *
    732         * @param dataset  the dataset (<code>null</code> not permitted).
    733         * @param includeInterval  a flag that determines whether or not the
    734         *                         y-interval is taken into account.
    735         *
    736         * @return The range (possibly <code>null</code>).
    737         */
    738        public static Range findRangeBounds(CategoryDataset dataset,
    739                                            boolean includeInterval) {
    740            if (dataset == null) {
    741                throw new IllegalArgumentException("Null 'dataset' argument.");
    742            }
    743            Range result = null;
    744            if (dataset instanceof RangeInfo) {
    745                RangeInfo info = (RangeInfo) dataset;
    746                result = info.getRangeBounds(includeInterval);
    747            }
    748            else {
    749                result = iterateCategoryRangeBounds(dataset, includeInterval);
    750            }
    751            return result;
    752        }
    753
    754        /**
    755         * Returns the range of values in the range for the dataset.  This method
    756         * is the partner for the {@link #findDomainBounds(XYDataset)} method.
    757         *
    758         * @param dataset  the dataset (<code>null</code> not permitted).
    759         *
    760         * @return The range (possibly <code>null</code>).
    761         */
    762        public static Range findRangeBounds(XYDataset dataset) {
    763            return findRangeBounds(dataset, true);
    764        }
    765
    766        /**
    767         * Returns the range of values in the range for the dataset.  This method
    768         * is the partner for the {@link #findDomainBounds(XYDataset)} method.
    769         *
    770         * @param dataset  the dataset (<code>null</code> not permitted).
    771         * @param includeInterval  a flag that determines whether or not the
    772         *                         y-interval is taken into account.
    773         *
    774         *
    775         * @return The range (possibly <code>null</code>).
    776         */
    777        public static Range findRangeBounds(XYDataset dataset,
    778                                            boolean includeInterval) {
    779            if (dataset == null) {
    780                throw new IllegalArgumentException("Null 'dataset' argument.");
    781            }
    782            Range result = null;
    783            if (dataset instanceof RangeInfo) {
    784                RangeInfo info = (RangeInfo) dataset;
    785                result = info.getRangeBounds(includeInterval);
    786            }
    787            else {
    788                result = iterateXYRangeBounds(dataset);
    789            }
    790            return result;
    791        }
    792
    793        /**
    794         * Iterates over the data item of the category dataset to find
    795         * the range bounds.
    796         *
    797         * @param dataset  the dataset (<code>null</code> not permitted).
    798         * @param includeInterval  a flag that determines whether or not the
    799         *                         y-interval is taken into account.
    800         *
    801         * @return The range (possibly <code>null</code>).
    802         */
    803        public static Range iterateCategoryRangeBounds(CategoryDataset dataset,
    804                boolean includeInterval) {
    805            double minimum = Double.POSITIVE_INFINITY;
    806            double maximum = Double.NEGATIVE_INFINITY;
    807            boolean interval = includeInterval
    808                               && dataset instanceof IntervalCategoryDataset;
    809            int rowCount = dataset.getRowCount();
    810            int columnCount = dataset.getColumnCount();
    811            for (int row = 0; row < rowCount; row++) {
    812                for (int column = 0; column < columnCount; column++) {
    813                    Number lvalue;
    814                    Number uvalue;
    815                    if (interval) {
    816                        IntervalCategoryDataset icd
    817                            = (IntervalCategoryDataset) dataset;
    818                        lvalue = icd.getStartValue(row, column);
    819                        uvalue = icd.getEndValue(row, column);
    820                    }
    821                    else {
    822                        lvalue = dataset.getValue(row, column);
    823                        uvalue = lvalue;
    824                    }
    825                    if (lvalue != null) {
    826                        minimum = Math.min(minimum, lvalue.doubleValue());
    827                    }
    828                    if (uvalue != null) {
    829                        maximum = Math.max(maximum, uvalue.doubleValue());
    830                    }
    831                }
    832            }
    833            if (minimum == Double.POSITIVE_INFINITY) {
    834                return null;
    835            }
    836            else {
    837                return new Range(minimum, maximum);
    838            }
    839        }
    840
    841        /**
    842         * Iterates over the data item of the xy dataset to find
    843         * the range bounds.
    844         *
    845         * @param dataset  the dataset (<code>null</code> not permitted).
    846         *
    847         * @return The range (possibly <code>null</code>).
    848         */
    849        public static Range iterateXYRangeBounds(XYDataset dataset) {
    850            double minimum = Double.POSITIVE_INFINITY;
    851            double maximum = Double.NEGATIVE_INFINITY;
    852            int seriesCount = dataset.getSeriesCount();
    853            for (int series = 0; series < seriesCount; series++) {
    854                int itemCount = dataset.getItemCount(series);
    855                for (int item = 0; item < itemCount; item++) {
    856                    double lvalue;
    857                    double uvalue;
    858                    if (dataset instanceof IntervalXYDataset) {
    859                        IntervalXYDataset intervalXYData
    860                            = (IntervalXYDataset) dataset;
    861                        lvalue = intervalXYData.getStartYValue(series, item);
    862                        uvalue = intervalXYData.getEndYValue(series, item);
    863                    }
    864                    else if (dataset instanceof OHLCDataset) {
    865                        OHLCDataset highLowData = (OHLCDataset) dataset;
    866                        lvalue = highLowData.getLowValue(series, item);
    867                        uvalue = highLowData.getHighValue(series, item);
    868                    }
    869                    else {
    870                        lvalue = dataset.getYValue(series, item);
    871                        uvalue = lvalue;
    872                    }
    873                    if (!Double.isNaN(lvalue)) {
    874                        minimum = Math.min(minimum, lvalue);
    875                    }
    876                    if (!Double.isNaN(uvalue)) {
    877                        maximum = Math.max(maximum, uvalue);
    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         * Finds the minimum domain (or X) value for the specified dataset.  This
    891         * is easy if the dataset implements the {@link DomainInfo} interface (a
    892         * good idea if there is an efficient way to determine the minimum value).
    893         * Otherwise, it involves iterating over the entire data-set.
    894         * <p>
    895         * Returns <code>null</code> if all the data values in the dataset are
    896         * <code>null</code>.
    897         *
    898         * @param dataset  the dataset (<code>null</code> not permitted).
    899         *
    900         * @return The minimum value (possibly <code>null</code>).
    901         */
    902        public static Number findMinimumDomainValue(XYDataset dataset) {
    903            if (dataset == null) {
    904                throw new IllegalArgumentException("Null 'dataset' argument.");
    905            }
    906            Number result = null;
    907            // if the dataset implements DomainInfo, life is easy
    908            if (dataset instanceof DomainInfo) {
    909                DomainInfo info = (DomainInfo) dataset;
    910                return new Double(info.getDomainLowerBound(true));
    911            }
    912            else {
    913                double minimum = Double.POSITIVE_INFINITY;
    914                int seriesCount = dataset.getSeriesCount();
    915                for (int series = 0; series < seriesCount; series++) {
    916                    int itemCount = dataset.getItemCount(series);
    917                    for (int item = 0; item < itemCount; item++) {
    918
    919                        double value;
    920                        if (dataset instanceof IntervalXYDataset) {
    921                            IntervalXYDataset intervalXYData
    922                                = (IntervalXYDataset) dataset;
    923                            value = intervalXYData.getStartXValue(series, item);
    924                        }
    925                        else {
    926                            value = dataset.getXValue(series, item);
    927                        }
    928                        if (!Double.isNaN(value)) {
    929                            minimum = Math.min(minimum, value);
    930                        }
    931
    932                    }
    933                }
    934                if (minimum == Double.POSITIVE_INFINITY) {
    935                    result = null;
    936                }
    937                else {
    938                    result = new Double(minimum);
    939                }
    940            }
    941
    942            return result;
    943        }
    944
    945        /**
    946         * Returns the maximum domain value for the specified dataset.  This is
    947         * easy if the dataset implements the {@link DomainInfo} interface (a good
    948         * idea if there is an efficient way to determine the maximum value).
    949         * Otherwise, it involves iterating over the entire data-set.  Returns
    950         * <code>null</code> if all the data values in the dataset are
    951         * <code>null</code>.
    952         *
    953         * @param dataset  the dataset (<code>null</code> not permitted).
    954         *
    955         * @return The maximum value (possibly <code>null</code>).
    956         */
    957        public static Number findMaximumDomainValue(XYDataset dataset) {
    958            if (dataset == null) {
    959                throw new IllegalArgumentException("Null 'dataset' argument.");
    960            }
    961            Number result = null;
    962            // if the dataset implements DomainInfo, life is easy
    963            if (dataset instanceof DomainInfo) {
    964                DomainInfo info = (DomainInfo) dataset;
    965                return new Double(info.getDomainUpperBound(true));
    966            }
    967
    968            // hasn't implemented DomainInfo, so iterate...
    969            else {
    970                double maximum = Double.NEGATIVE_INFINITY;
    971                int seriesCount = dataset.getSeriesCount();
    972                for (int series = 0; series < seriesCount; series++) {
    973                    int itemCount = dataset.getItemCount(series);
    974                    for (int item = 0; item < itemCount; item++) {
    975
    976                        double value;
    977                        if (dataset instanceof IntervalXYDataset) {
    978                            IntervalXYDataset intervalXYData
    979                                = (IntervalXYDataset) dataset;
    980                            value = intervalXYData.getEndXValue(series, item);
    981                        }
    982                        else {
    983                            value = dataset.getXValue(series, item);
    984                        }
    985                        if (!Double.isNaN(value)) {
    986                            maximum = Math.max(maximum, value);
    987                        }
    988                    }
    989                }
    990                if (maximum == Double.NEGATIVE_INFINITY) {
    991                    result = null;
    992                }
    993                else {
    994                    result = new Double(maximum);
    995                }
    996
    997            }
    998
    999            return result;
    1000        }
    1001
    1002        /**
    1003         * Returns the minimum range value for the specified dataset.  This is
    1004         * easy if the dataset implements the {@link RangeInfo} interface (a good
    1005         * idea if there is an efficient way to determine the minimum value).
    1006         * Otherwise, it involves iterating over the entire data-set.  Returns
    1007         * <code>null</code> if all the data values in the dataset are
    1008         * <code>null</code>.
    1009         *
    1010         * @param dataset  the dataset (<code>null</code> not permitted).
    1011         *
    1012         * @return The minimum value (possibly <code>null</code>).
    1013         */
    1014        public static Number findMinimumRangeValue(CategoryDataset dataset) {
    1015
    1016            // check parameters...
    1017            if (dataset == null) {
    1018                throw new IllegalArgumentException("Null 'dataset' argument.");
    1019            }
    1020
    1021            // work out the minimum value...
    1022            if (dataset instanceof RangeInfo) {
    1023                RangeInfo info = (RangeInfo) dataset;
    1024                return new Double(info.getRangeLowerBound(true));
    1025            }
    1026
    1027            // hasn't implemented RangeInfo, so we'll have to iterate...
    1028            else {
    1029                double minimum = Double.POSITIVE_INFINITY;
    1030                int seriesCount = dataset.getRowCount();
    1031                int itemCount = dataset.getColumnCount();
    1032                for (int series = 0; series < seriesCount; series++) {
    1033                    for (int item = 0; item < itemCount; item++) {
    1034                        Number value;
    1035                        if (dataset instanceof IntervalCategoryDataset) {
    1036                            IntervalCategoryDataset icd
    1037                                = (IntervalCategoryDataset) dataset;
    1038                            value = icd.getStartValue(series, item);
    1039                        }
    1040                        else {
    1041                            value = dataset.getValue(series, item);
    1042                        }
    1043                        if (value != null) {
    1044                            minimum = Math.min(minimum, value.doubleValue());
    1045                        }
    1046                    }
    1047                }
    1048                if (minimum == Double.POSITIVE_INFINITY) {
    1049                    return null;
    1050                }
    1051                else {
    1052                    return new Double(minimum);
    1053                }
    1054
    1055            }
    1056
    1057        }
    1058
    1059        /**
    1060         * Returns the minimum range value for the specified dataset.  This is
    1061         * easy if the dataset implements the {@link RangeInfo} interface (a good
    1062         * idea if there is an efficient way to determine the minimum value).
    1063         * Otherwise, it involves iterating over the entire data-set.  Returns
    1064         * <code>null</code> if all the data values in the dataset are
    1065         * <code>null</code>.
    1066         *
    1067         * @param dataset  the dataset (<code>null</code> not permitted).
    1068         *
    1069         * @return The minimum value (possibly <code>null</code>).
    1070         */
    1071        public static Number findMinimumRangeValue(XYDataset dataset) {
    1072
    1073            if (dataset == null) {
    1074                throw new IllegalArgumentException("Null 'dataset' argument.");
    1075            }
    1076
    1077            // work out the minimum value...
    1078            if (dataset instanceof RangeInfo) {
    1079                RangeInfo info = (RangeInfo) dataset;
    1080                return new Double(info.getRangeLowerBound(true));
    1081            }
    1082
    1083            // hasn't implemented RangeInfo, so we'll have to iterate...
    1084            else {
    1085                double minimum = Double.POSITIVE_INFINITY;
    1086                int seriesCount = dataset.getSeriesCount();
    1087                for (int series = 0; series < seriesCount; series++) {
    1088                    int itemCount = dataset.getItemCount(series);
    1089                    for (int item = 0; item < itemCount; item++) {
    1090
    1091                        double value;
    1092                        if (dataset instanceof IntervalXYDataset) {
    1093                            IntervalXYDataset intervalXYData
    1094                                = (IntervalXYDataset) dataset;
    1095                            value = intervalXYData.getStartYValue(series, item);
    1096                        }
    1097                        else if (dataset instanceof OHLCDataset) {
    1098                            OHLCDataset highLowData = (OHLCDataset) dataset;
    1099                            value = highLowData.getLowValue(series, item);
    1100                        }
    1101                        else {
    1102                            value = dataset.getYValue(series, item);
    1103                        }
    1104                        if (!Double.isNaN(value)) {
    1105                            minimum = Math.min(minimum, value);
    1106                        }
    1107
    1108                    }
    1109                }
    1110                if (minimum == Double.POSITIVE_INFINITY) {
    1111                    return null;
    1112                }
    1113                else {
    1114                    return new Double(minimum);
    1115                }
    1116
    1117            }
    1118
    1119        }
    1120
    1121        /**
    1122         * Returns the maximum range value for the specified dataset.  This is easy
    1123         * if the dataset implements the {@link RangeInfo} interface (a good idea
    1124         * if there is an efficient way to determine the maximum value).
    1125         * Otherwise, it involves iterating over the entire data-set.  Returns
    1126         * <code>null</code> if all the data values are <code>null</code>.
    1127         *
    1128         * @param dataset  the dataset (<code>null</code> not permitted).
    1129         *
    1130         * @return The maximum value (possibly <code>null</code>).
    1131         */
    1132        public static Number findMaximumRangeValue(CategoryDataset dataset) {
    1133
    1134            if (dataset == null) {
    1135                throw new IllegalArgumentException("Null 'dataset' argument.");
    1136            }
    1137
    1138            // work out the minimum value...
    1139            if (dataset instanceof RangeInfo) {
    1140                RangeInfo info = (RangeInfo) dataset;
    1141                return new Double(info.getRangeUpperBound(true));
    1142            }
    1143
    1144            // hasn't implemented RangeInfo, so we'll have to iterate...
    1145            else {
    1146
    1147                double maximum = Double.NEGATIVE_INFINITY;
    1148                int seriesCount = dataset.getRowCount();
    1149                int itemCount = dataset.getColumnCount();
    1150                for (int series = 0; series < seriesCount; series++) {
    1151                    for (int item = 0; item < itemCount; item++) {
    1152                        Number value;
    1153                        if (dataset instanceof IntervalCategoryDataset) {
    1154                            IntervalCategoryDataset icd
    1155                                = (IntervalCategoryDataset) dataset;
    1156                            value = icd.getEndValue(series, item);
    1157                        }
    1158                        else {
    1159                            value = dataset.getValue(series, item);
    1160                        }
    1161                        if (value != null) {
    1162                            maximum = Math.max(maximum, value.doubleValue());
    1163                        }
    1164                    }
    1165                }
    1166                if (maximum == Double.NEGATIVE_INFINITY) {
    1167                    return null;
    1168                }
    1169                else {
    1170                    return new Double(maximum);
    1171                }
    1172
    1173            }
    1174
    1175        }
    1176
    1177        /**
    1178         * Returns the maximum range value for the specified dataset.  This is
    1179         * easy if the dataset implements the {@link RangeInfo} interface (a good
    1180         * idea if there is an efficient way to determine the maximum value).
    1181         * Otherwise, it involves iterating over the entire data-set.  Returns
    1182         * <code>null</code> if all the data values are <code>null</code>.
    1183         *
    1184         * @param dataset  the dataset (<code>null</code> not permitted).
    1185         *
    1186         * @return The maximum value (possibly <code>null</code>).
    1187         */
    1188        public static Number findMaximumRangeValue(XYDataset dataset) {
    1189
    1190            if (dataset == null) {
    1191                throw new IllegalArgumentException("Null 'dataset' argument.");
    1192            }
    1193
    1194            // work out the minimum value...
    1195            if (dataset instanceof RangeInfo) {
    1196                RangeInfo info = (RangeInfo) dataset;
    1197                return new Double(info.getRangeUpperBound(true));
    1198            }
    1199
    1200            // hasn't implemented RangeInfo, so we'll have to iterate...
    1201            else  {
    1202
    1203                double maximum = Double.NEGATIVE_INFINITY;
    1204                int seriesCount = dataset.getSeriesCount();
    1205                for (int series = 0; series < seriesCount; series++) {
    1206                    int itemCount = dataset.getItemCount(series);
    1207                    for (int item = 0; item < itemCount; item++) {
    1208                        double value;
    1209                        if (dataset instanceof IntervalXYDataset) {
    1210                            IntervalXYDataset intervalXYData
    1211                                = (IntervalXYDataset) dataset;
    1212                            value = intervalXYData.getEndYValue(series, item);
    1213                        }
    1214                        else if (dataset instanceof OHLCDataset) {
    1215                            OHLCDataset highLowData = (OHLCDataset) dataset;
    1216                            value = highLowData.getHighValue(series, item);
    1217                        }
    1218                        else {
    1219                            value = dataset.getYValue(series, item);
    1220                        }
    1221                        if (!Double.isNaN(value)) {
    1222                            maximum = Math.max(maximum, value);
    1223                        }
    1224                    }
    1225                }
    1226                if (maximum == Double.NEGATIVE_INFINITY) {
    1227                    return null;
    1228                }
    1229                else {
    1230                    return new Double(maximum);
    1231                }
    1232
    1233            }
    1234
    1235        }
    1236
    1237        /**
    1238         * Returns the minimum and maximum values for the dataset's range
    1239         * (y-values), assuming that the series in one category are stacked.
    1240         *
    1241         * @param dataset  the dataset (<code>null</code> not permitted).
    1242         *
    1243         * @return The range (<code>null</code> if the dataset contains no values).
    1244         */
    1245        public static Range findStackedRangeBounds(CategoryDataset dataset) {
    1246            return findStackedRangeBounds(dataset, 0.0);
    1247        }
    1248
    1249        /**
    1250         * Returns the minimum and maximum values for the dataset's range
    1251         * (y-values), assuming that the series in one category are stacked.
    1252         *
    1253         * @param dataset  the dataset (<code>null</code> not permitted).
    1254         * @param base  the base value for the bars.
    1255         *
    1256         * @return The range (<code>null</code> if the dataset contains no values).
    1257         */
    1258        public static Range findStackedRangeBounds(CategoryDataset dataset,
    1259                double base) {
    1260            if (dataset == null) {
    1261                throw new IllegalArgumentException("Null 'dataset' argument.");
    1262            }
    1263            Range result = null;
    1264            double minimum = Double.POSITIVE_INFINITY;
    1265            double maximum = Double.NEGATIVE_INFINITY;
    1266            int categoryCount = dataset.getColumnCount();
    1267            for (int item = 0; item < categoryCount; item++) {
    1268                double positive = base;
    1269                double negative = base;
    1270                int seriesCount = dataset.getRowCount();
    1271                for (int series = 0; series < seriesCount; series++) {
    1272                    Number number = dataset.getValue(series, item);
    1273                    if (number != null) {
    1274                        double value = number.doubleValue();
    1275                        if (value > 0.0) {
    1276                            positive = positive + value;
    1277                        }
    1278                        if (value < 0.0) {
    1279                            negative = negative + value;
    1280                            // '+', remember value is negative
    1281                        }
    1282                    }
    1283                }
    1284                minimum = Math.min(minimum, negative);
    1285                maximum = Math.max(maximum, positive);
    1286            }
    1287            if (minimum <= maximum) {
    1288                result = new Range(minimum, maximum);
    1289            }
    1290            return result;
    1291
    1292        }
    1293
    1294        /**
    1295         * Returns the minimum and maximum values for the dataset's range
    1296         * (y-values), assuming that the series in one category are stacked.
    1297         *
    1298         * @param dataset  the dataset.
    1299         * @param map  a structure that maps series to groups.
    1300         *
    1301         * @return The value range (<code>null</code> if the dataset contains no
    1302         *         values).
    1303         */
    1304        public static Range findStackedRangeBounds(CategoryDataset dataset,
    1305                                                   KeyToGroupMap map) {
    1306
    1307            Range result = null;
    1308            if (dataset != null) {
    1309
    1310                // create an array holding the group indices...
    1311                int[] groupIndex = new int[dataset.getRowCount()];
    1312                for (int i = 0; i < dataset.getRowCount(); i++) {
    1313                    groupIndex[i] = map.getGroupIndex(
    1314                        map.getGroup(dataset.getRowKey(i))
    1315                    );
    1316                }
    1317
    1318                // minimum and maximum for each group...
    1319                int groupCount = map.getGroupCount();
    1320                double[] minimum = new double[groupCount];
    1321                double[] maximum = new double[groupCount];
    1322
    1323                int categoryCount = dataset.getColumnCount();
    1324                for (int item = 0; item < categoryCount; item++) {
    1325                    double[] positive = new double[groupCount];
    1326                    double[] negative = new double[groupCount];
    1327                    int seriesCount = dataset.getRowCount();
    1328                    for (int series = 0; series < seriesCount; series++) {
    1329                        Number number = dataset.getValue(series, item);
    1330                        if (number != null) {
    1331                            double value = number.doubleValue();
    1332                            if (value > 0.0) {
    1333                                positive[groupIndex[series]]
    1334                                     = positive[groupIndex[series]] + value;
    1335                            }
    1336                            if (value < 0.0) {
    1337                                negative[groupIndex[series]]
    1338                                     = negative[groupIndex[series]] + value;
    1339                                     // '+', remember value is negative
    1340                            }
    1341                        }
    1342                    }
    1343                    for (int g = 0; g < groupCount; g++) {
    1344                        minimum[g] = Math.min(minimum[g], negative[g]);
    1345                        maximum[g] = Math.max(maximum[g], positive[g]);
    1346                    }
    1347                }
    1348                for (int j = 0; j < groupCount; j++) {
    1349                    result = Range.combine(
    1350                        result, new Range(minimum[j], maximum[j])
    1351                    );
    1352                }
    1353            }
    1354            return result;
    1355
    1356        }
    1357
    1358        /**
    1359         * Returns the minimum value in the dataset range, assuming that values in
    1360         * each category are "stacked".
    1361         *
    1362         * @param dataset  the dataset.
    1363         *
    1364         * @return The minimum value.
    1365         */
    1366        public static Number findMinimumStackedRangeValue(CategoryDataset dataset) {
    1367
    1368            Number result = null;
    1369            if (dataset != null) {
    1370                double minimum = 0.0;
    1371                int categoryCount = dataset.getRowCount();
    1372                for (int item = 0; item < categoryCount; item++) {
    1373                    double total = 0.0;
    1374
    1375                    int seriesCount = dataset.getColumnCount();
    1376                    for (int series = 0; series < seriesCount; series++) {
    1377                        Number number = dataset.getValue(series, item);
    1378                        if (number != null) {
    1379                            double value = number.doubleValue();
    1380                            if (value < 0.0) {
    1381                                total = total + value;
    1382                                // '+', remember value is negative
    1383                            }
    1384                        }
    1385                    }
    1386                    minimum = Math.min(minimum, total);
    1387
    1388                }
    1389                result = new Double(minimum);
    1390            }
    1391            return result;
    1392
    1393        }
    1394
    1395        /**
    1396         * Returns the maximum value in the dataset range, assuming that values in
    1397         * each category are "stacked".
    1398         *
    1399         * @param dataset  the dataset (<code>null</code> permitted).
    1400         *
    1401         * @return The maximum value (possibly <code>null</code>).
    1402         */
    1403        public static Number findMaximumStackedRangeValue(CategoryDataset dataset) {
    1404
    1405            Number result = null;
    1406
    1407            if (dataset != null) {
    1408                double maximum = 0.0;
    1409                int categoryCount = dataset.getColumnCount();
    1410                for (int item = 0; item < categoryCount; item++) {
    1411                    double total = 0.0;
    1412                    int seriesCount = dataset.getRowCount();
    1413                    for (int series = 0; series < seriesCount; series++) {
    1414                        Number number = dataset.getValue(series, item);
    1415                        if (number != null) {
    1416                            double value = number.doubleValue();
    1417                            if (value > 0.0) {
    1418                                total = total + value;
    1419                            }
    1420                        }
    1421                    }
    1422                    maximum = Math.max(maximum, total);
    1423                }
    1424                result = new Double(maximum);
    1425            }
    1426
    1427            return result;
    1428
    1429        }
    1430
    1431        /**
    1432         * Returns the minimum and maximum values for the dataset's range,
    1433         * assuming that the series are stacked.
    1434         *
    1435         * @param dataset  the dataset (<code>null</code> not permitted).
    1436         *
    1437         * @return The range ([0.0, 0.0] if the dataset contains no values).
    1438         */
    1439        public static Range findStackedRangeBounds(TableXYDataset dataset) {
    1440            return findStackedRangeBounds(dataset, 0.0);
    1441        }
    1442
    1443        /**
    1444         * Returns the minimum and maximum values for the dataset's range,
    1445         * assuming that the series are stacked, using the specified base value.
    1446         *
    1447         * @param dataset  the dataset (<code>null</code> not permitted).
    1448         * @param base  the base value.
    1449         *
    1450         * @return The range (<code>null</code> if the dataset contains no values).
    1451         */
    1452        public static Range findStackedRangeBounds(TableXYDataset dataset,
    1453                                                   double base) {
    1454            if (dataset == null) {
    1455                throw new IllegalArgumentException("Null 'dataset' argument.");
    1456            }
    1457            double minimum = base;
    1458            double maximum = base;
    1459            for (int itemNo = 0; itemNo < dataset.getItemCount(); itemNo++) {
    1460                double positive = base;
    1461                double negative = base;
    1462                int seriesCount = dataset.getSeriesCount();
    1463                for (int seriesNo = 0; seriesNo < seriesCount; seriesNo++) {
    1464                    double y = dataset.getYValue(seriesNo, itemNo);
    1465                    if (!Double.isNaN(y)) {
    1466                        if (y > 0.0) {
    1467                            positive += y;
    1468                        }
    1469                        else {
    1470                            negative += y;
    1471                        }
    1472                    }
    1473                }
    1474                if (positive > maximum) {
    1475                    maximum = positive;
    1476                }
    1477                if (negative < minimum) {
    1478                    minimum = negative;
    1479                }
    1480            }
    1481            if (minimum <= maximum) {
    1482                return new Range(minimum, maximum);
    1483            }
    1484            else {
    1485                return null;
    1486            }
    1487        }
    1488
    1489        /**
    1490         * Calculates the total for the y-values in all series for a given item
    1491         * index.
    1492         *
    1493         * @param dataset  the dataset.
    1494         * @param item  the item index.
    1495         *
    1496         * @return The total.
    1497         *
    1498         * @since 1.0.5
    1499         */
    1500        public static double calculateStackTotal(TableXYDataset dataset, int item) {
    1501            double total = 0.0;
    1502            int seriesCount = dataset.getSeriesCount();
    1503            for (int s = 0; s < seriesCount; s++) {
    1504                double value = dataset.getYValue(s, item);
    1505                if (!Double.isNaN(value)) {
    1506                    total = total + value;
    1507                }
    1508            }
    1509            return total;
    1510        }
    1511
    1512        /**
    1513         * Calculates the range of values for a dataset where each item is the
    1514         * running total of the items for the current series.
    1515         *
    1516         * @param dataset  the dataset (<code>null</code> not permitted).
    1517         *
    1518         * @return The range.
    1519         */
    1520        public static Range findCumulativeRangeBounds(CategoryDataset dataset) {
    1521
    1522            if (dataset == null) {
    1523                throw new IllegalArgumentException("Null 'dataset' argument.");
    1524            }
    1525
    1526            boolean allItemsNull = true; // we'll set this to false if there is at
    1527                                         // least one non-null data item...
    1528            double minimum = 0.0;
    1529            double maximum = 0.0;
    1530            for (int row = 0; row < dataset.getRowCount(); row++) {
    1531                double runningTotal = 0.0;
    1532                for (int column = 0; column < dataset.getColumnCount() - 1;
    1533                     column++) {
    1534                    Number n = dataset.getValue(row, column);
    1535                    if (n != null) {
    1536                        allItemsNull = false;
    1537                        double value = n.doubleValue();
    1538                        runningTotal = runningTotal + value;
    1539                        minimum = Math.min(minimum, runningTotal);
    1540                        maximum = Math.max(maximum, runningTotal);
    1541                    }
    1542                }
    1543            }
    1544            if (!allItemsNull) {
    1545                return new Range(minimum, maximum);
    1546            }
    1547            else {
    1548                return null;
    1549            }
    1550
    1551        }
    1552
    1553    }