001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2014, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * ---------------------
028 * DatasetUtilities.java
029 * ---------------------
030 * (C) Copyright 2000-2014, 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 *                   Peter Kolb (patch 2791407);
040 *                   Martin Hoeller (patch 2952086);
041 *
042 * Changes (from 18-Sep-2001)
043 * --------------------------
044 * 18-Sep-2001 : Added standard header and fixed DOS encoding problem (DG);
045 * 22-Oct-2001 : Renamed DataSource.java --> Dataset.java etc. (DG);
046 * 15-Nov-2001 : Moved to package com.jrefinery.data.* in the JCommon class
047 *               library (DG);
048 *               Changed to handle null values from datasets (DG);
049 *               Bug fix (thanks to Andrzej Porebski) - initial value now set
050 *               to positive or negative infinity when iterating (DG);
051 * 22-Nov-2001 : Datasets with containing no data now return null for min and
052 *               max calculations (DG);
053 * 13-Dec-2001 : Extended to handle HighLowDataset and IntervalXYDataset (DG);
054 * 15-Feb-2002 : Added getMinimumStackedRangeValue() and
055 *               getMaximumStackedRangeValue() (DG);
056 * 28-Feb-2002 : Renamed Datasets.java --> DatasetUtilities.java (DG);
057 * 18-Mar-2002 : Fixed bug in min/max domain calculation for datasets that
058 *               implement the CategoryDataset interface AND the XYDataset
059 *               interface at the same time.  Thanks to Jonathan Nash for the
060 *               fix (DG);
061 * 23-Apr-2002 : Added getDomainExtent() and getRangeExtent() methods (DG);
062 * 13-Jun-2002 : Modified range measurements to handle
063 *               IntervalCategoryDataset (DG);
064 * 12-Jul-2002 : Method name change in DomainInfo interface (DG);
065 * 30-Jul-2002 : Added pie dataset summation method (DG);
066 * 01-Oct-2002 : Added a method for constructing an XYDataset from a Function2D
067 *               instance (DG);
068 * 24-Oct-2002 : Amendments required following changes to the CategoryDataset
069 *               interface (DG);
070 * 18-Nov-2002 : Changed CategoryDataset to TableDataset (DG);
071 * 04-Mar-2003 : Added isEmpty(XYDataset) method (DG);
072 * 05-Mar-2003 : Added a method for creating a CategoryDataset from a
073 *               KeyedValues instance (DG);
074 * 15-May-2003 : Renamed isEmpty --> isEmptyOrNull (DG);
075 * 25-Jun-2003 : Added limitPieDataset methods (RA);
076 * 26-Jun-2003 : Modified getDomainExtent() method to accept null datasets (DG);
077 * 27-Jul-2003 : Added getStackedRangeExtent(TableXYDataset data) (RA);
078 * 18-Aug-2003 : getStackedRangeExtent(TableXYDataset data) now handles null
079 *               values (RA);
080 * 02-Sep-2003 : Added method to check for null or empty PieDataset (DG);
081 * 18-Sep-2003 : Fix for bug 803660 (getMaximumRangeValue for
082 *               CategoryDataset) (DG);
083 * 20-Oct-2003 : Added getCumulativeRangeExtent() method (DG);
084 * 09-Jan-2003 : Added argument checking code to the createCategoryDataset()
085 *               method (DG);
086 * 23-Mar-2004 : Fixed bug in getMaximumStackedRangeValue() method (DG);
087 * 31-Mar-2004 : Exposed the extent iteration algorithms to use one of them and
088 *               applied noninstantiation pattern (AS);
089 * 11-May-2004 : Renamed getPieDatasetTotal --> calculatePieDatasetTotal (DG);
090 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with getYValue();
091 * 24-Aug-2004 : Added argument checks to createCategoryDataset() method (DG);
092 * 04-Oct-2004 : Renamed ArrayUtils --> ArrayUtilities (DG);
093 * 06-Oct-2004 : Renamed findDomainExtent() --> findDomainBounds(),
094 *               findRangeExtent() --> findRangeBounds() (DG);
095 * 07-Jan-2005 : Renamed findStackedRangeExtent() --> findStackedRangeBounds(),
096 *               findCumulativeRangeExtent() --> findCumulativeRangeBounds(),
097 *               iterateXYRangeExtent() --> iterateXYRangeBounds(),
098 *               removed deprecated methods (DG);
099 * 03-Feb-2005 : The findStackedRangeBounds() methods now return null for
100 *               empty datasets (DG);
101 * 03-Mar-2005 : Moved createNumberArray() and createNumberArray2D() methods
102 *               from DatasetUtilities --> DataUtilities (DG);
103 * 22-Sep-2005 : Added new findStackedRangeBounds() method that takes base
104 *               argument (DG);
105 * ------------- JFREECHART 1.0.x ---------------------------------------------
106 * 15-Mar-2007 : Added calculateStackTotal() method (DG);
107 * 27-Mar-2008 : Fixed bug in findCumulativeRangeBounds() method (DG);
108 * 28-Mar-2008 : Fixed sample count in sampleFunction2D() method, renamed
109 *               iterateXYRangeBounds() --> iterateRangeBounds(XYDataset), and
110 *               fixed a bug in findRangeBounds(XYDataset, false) (DG);
111 * 28-Mar-2008 : Applied a variation of patch 1925366 (from Rafal Skalny) for
112 *               slightly more efficient iterateRangeBounds() methods (DG);
113 * 08-Apr-2008 : Fixed typo in iterateRangeBounds() (DG);
114 * 08-Oct-2008 : Applied patch 2131001 by Jerome David, with some modifications
115 *               and additions and some new unit tests (DG);
116 * 12-Feb-2009 : Added sampleFunction2DToSeries() method (DG);
117 * 27-Mar-2009 : Added new methods to find domain and range bounds taking into
118 *               account hidden series (DG);
119 * 01-Apr-2009 : Handle a StatisticalCategoryDataset in
120 *               iterateToFindRangeBounds() (DG);
121 * 16-May-2009 : Patch 2791407 - fix iterateToFindRangeBounds for
122 *               MultiValueCategoryDataset (PK);
123 * 10-Sep-2009 : Fix bug 2849731 for IntervalCategoryDataset (DG);
124 * 16-Feb-2010 : Patch 2952086 - find z-bounds (MH);
125 * 02-Jul-2013 : Use ParamChecks (DG);
126 * 
127 */
128
129package org.jfree.data.general;
130
131import java.util.ArrayList;
132import java.util.Iterator;
133import java.util.List;
134import org.jfree.chart.util.ParamChecks;
135
136import org.jfree.data.DomainInfo;
137import org.jfree.data.DomainOrder;
138import org.jfree.data.KeyToGroupMap;
139import org.jfree.data.KeyedValues;
140import org.jfree.data.Range;
141import org.jfree.data.RangeInfo;
142import org.jfree.data.category.CategoryDataset;
143import org.jfree.data.category.CategoryRangeInfo;
144import org.jfree.data.category.DefaultCategoryDataset;
145import org.jfree.data.category.IntervalCategoryDataset;
146import org.jfree.data.function.Function2D;
147import org.jfree.data.statistics.BoxAndWhiskerCategoryDataset;
148import org.jfree.data.statistics.BoxAndWhiskerXYDataset;
149import org.jfree.data.statistics.MultiValueCategoryDataset;
150import org.jfree.data.statistics.StatisticalCategoryDataset;
151import org.jfree.data.xy.IntervalXYDataset;
152import org.jfree.data.xy.OHLCDataset;
153import org.jfree.data.xy.TableXYDataset;
154import org.jfree.data.xy.XYDataset;
155import org.jfree.data.xy.XYDomainInfo;
156import org.jfree.data.xy.XYRangeInfo;
157import org.jfree.data.xy.XYSeries;
158import org.jfree.data.xy.XYSeriesCollection;
159import org.jfree.data.xy.XYZDataset;
160import org.jfree.util.ArrayUtilities;
161
162/**
163 * A collection of useful static methods relating to datasets.
164 */
165public final class DatasetUtilities {
166
167    /**
168     * Private constructor for non-instanceability.
169     */
170    private DatasetUtilities() {
171        // now try to instantiate this ;-)
172    }
173
174    /**
175     * Calculates the total of all the values in a {@link PieDataset}.  If
176     * the dataset contains negative or <code>null</code> values, they are
177     * ignored.
178     *
179     * @param dataset  the dataset (<code>null</code> not permitted).
180     *
181     * @return The total.
182     */
183    public static double calculatePieDatasetTotal(PieDataset dataset) {
184        ParamChecks.nullNotPermitted(dataset, "dataset");
185        List keys = dataset.getKeys();
186        double totalValue = 0;
187        Iterator iterator = keys.iterator();
188        while (iterator.hasNext()) {
189            Comparable current = (Comparable) iterator.next();
190            if (current != null) {
191                Number value = dataset.getValue(current);
192                double v = 0.0;
193                if (value != null) {
194                    v = value.doubleValue();
195                }
196                if (v > 0) {
197                    totalValue = totalValue + v;
198                }
199            }
200        }
201        return totalValue;
202    }
203
204    /**
205     * Creates a pie dataset from a table dataset by taking all the values
206     * for a single row.
207     *
208     * @param dataset  the dataset (<code>null</code> not permitted).
209     * @param rowKey  the row key.
210     *
211     * @return A pie dataset.
212     */
213    public static PieDataset createPieDatasetForRow(CategoryDataset dataset,
214                                                    Comparable rowKey) {
215        int row = dataset.getRowIndex(rowKey);
216        return createPieDatasetForRow(dataset, row);
217    }
218
219    /**
220     * Creates a pie dataset from a table dataset by taking all the values
221     * for a single row.
222     *
223     * @param dataset  the dataset (<code>null</code> not permitted).
224     * @param row  the row (zero-based index).
225     *
226     * @return A pie dataset.
227     */
228    public static PieDataset createPieDatasetForRow(CategoryDataset dataset,
229                                                    int row) {
230        DefaultPieDataset result = new DefaultPieDataset();
231        int columnCount = dataset.getColumnCount();
232        for (int current = 0; current < columnCount; current++) {
233            Comparable columnKey = dataset.getColumnKey(current);
234            result.setValue(columnKey, dataset.getValue(row, current));
235        }
236        return result;
237    }
238
239    /**
240     * Creates a pie dataset from a table dataset by taking all the values
241     * for a single column.
242     *
243     * @param dataset  the dataset (<code>null</code> not permitted).
244     * @param columnKey  the column key.
245     *
246     * @return A pie dataset.
247     */
248    public static PieDataset createPieDatasetForColumn(CategoryDataset dataset,
249                                                       Comparable columnKey) {
250        int column = dataset.getColumnIndex(columnKey);
251        return createPieDatasetForColumn(dataset, column);
252    }
253
254    /**
255     * Creates a pie dataset from a {@link CategoryDataset} by taking all the
256     * values for a single column.
257     *
258     * @param dataset  the dataset (<code>null</code> not permitted).
259     * @param column  the column (zero-based index).
260     *
261     * @return A pie dataset.
262     */
263    public static PieDataset createPieDatasetForColumn(CategoryDataset dataset,
264                                                       int column) {
265        DefaultPieDataset result = new DefaultPieDataset();
266        int rowCount = dataset.getRowCount();
267        for (int i = 0; i < rowCount; i++) {
268            Comparable rowKey = dataset.getRowKey(i);
269            result.setValue(rowKey, dataset.getValue(i, column));
270        }
271        return result;
272    }
273
274    /**
275     * Creates a new pie dataset based on the supplied dataset, but modified
276     * by aggregating all the low value items (those whose value is lower
277     * than the <code>percentThreshold</code>) into a single item with the
278     * key "Other".
279     *
280     * @param source  the source dataset (<code>null</code> not permitted).
281     * @param key  a new key for the aggregated items (<code>null</code> not
282     *             permitted).
283     * @param minimumPercent  the percent threshold.
284     *
285     * @return The pie dataset with (possibly) aggregated items.
286     */
287    public static PieDataset createConsolidatedPieDataset(PieDataset source,
288            Comparable key, double minimumPercent) {
289        return DatasetUtilities.createConsolidatedPieDataset(source, key,
290                minimumPercent, 2);
291    }
292
293    /**
294     * Creates a new pie dataset based on the supplied dataset, but modified
295     * by aggregating all the low value items (those whose value is lower
296     * than the <code>percentThreshold</code>) into a single item.  The
297     * aggregated items are assigned the specified key.  Aggregation only
298     * occurs if there are at least <code>minItems</code> items to aggregate.
299     *
300     * @param source  the source dataset (<code>null</code> not permitted).
301     * @param key  the key to represent the aggregated items.
302     * @param minimumPercent  the percent threshold (ten percent is 0.10).
303     * @param minItems  only aggregate low values if there are at least this
304     *                  many.
305     *
306     * @return The pie dataset with (possibly) aggregated items.
307     */
308    public static PieDataset createConsolidatedPieDataset(PieDataset source,
309            Comparable key, double minimumPercent, int minItems) {
310
311        DefaultPieDataset result = new DefaultPieDataset();
312        double total = DatasetUtilities.calculatePieDatasetTotal(source);
313
314        //  Iterate and find all keys below threshold percentThreshold
315        List keys = source.getKeys();
316        ArrayList otherKeys = new ArrayList();
317        Iterator iterator = keys.iterator();
318        while (iterator.hasNext()) {
319            Comparable currentKey = (Comparable) iterator.next();
320            Number dataValue = source.getValue(currentKey);
321            if (dataValue != null) {
322                double value = dataValue.doubleValue();
323                if (value / total < minimumPercent) {
324                    otherKeys.add(currentKey);
325                }
326            }
327        }
328
329        //  Create new dataset with keys above threshold percentThreshold
330        iterator = keys.iterator();
331        double otherValue = 0;
332        while (iterator.hasNext()) {
333            Comparable currentKey = (Comparable) iterator.next();
334            Number dataValue = source.getValue(currentKey);
335            if (dataValue != null) {
336                if (otherKeys.contains(currentKey)
337                    && otherKeys.size() >= minItems) {
338                    //  Do not add key to dataset
339                    otherValue += dataValue.doubleValue();
340                }
341                else {
342                    //  Add key to dataset
343                    result.setValue(currentKey, dataValue);
344                }
345            }
346        }
347        //  Add other category if applicable
348        if (otherKeys.size() >= minItems) {
349            result.setValue(key, otherValue);
350        }
351        return result;
352    }
353
354    /**
355     * Creates a {@link CategoryDataset} that contains a copy of the data in an
356     * array (instances of <code>Double</code> are created to represent the
357     * data items).
358     * <p>
359     * Row and column keys are created by appending 0, 1, 2, ... to the
360     * supplied prefixes.
361     *
362     * @param rowKeyPrefix  the row key prefix.
363     * @param columnKeyPrefix  the column key prefix.
364     * @param data  the data.
365     *
366     * @return The dataset.
367     */
368    public static CategoryDataset createCategoryDataset(String rowKeyPrefix,
369            String columnKeyPrefix, double[][] data) {
370
371        DefaultCategoryDataset result = new DefaultCategoryDataset();
372        for (int r = 0; r < data.length; r++) {
373            String rowKey = rowKeyPrefix + (r + 1);
374            for (int c = 0; c < data[r].length; c++) {
375                String columnKey = columnKeyPrefix + (c + 1);
376                result.addValue(new Double(data[r][c]), rowKey, columnKey);
377            }
378        }
379        return result;
380
381    }
382
383    /**
384     * Creates a {@link CategoryDataset} that contains a copy of the data in
385     * an array.
386     * <p>
387     * Row and column keys are created by appending 0, 1, 2, ... to the
388     * supplied prefixes.
389     *
390     * @param rowKeyPrefix  the row key prefix.
391     * @param columnKeyPrefix  the column key prefix.
392     * @param data  the data.
393     *
394     * @return The dataset.
395     */
396    public static CategoryDataset createCategoryDataset(String rowKeyPrefix,
397            String columnKeyPrefix, Number[][] data) {
398
399        DefaultCategoryDataset result = new DefaultCategoryDataset();
400        for (int r = 0; r < data.length; r++) {
401            String rowKey = rowKeyPrefix + (r + 1);
402            for (int c = 0; c < data[r].length; c++) {
403                String columnKey = columnKeyPrefix + (c + 1);
404                result.addValue(data[r][c], rowKey, columnKey);
405            }
406        }
407        return result;
408
409    }
410
411    /**
412     * Creates a {@link CategoryDataset} that contains a copy of the data in
413     * an array (instances of <code>Double</code> are created to represent the
414     * data items).
415     * <p>
416     * Row and column keys are taken from the supplied arrays.
417     *
418     * @param rowKeys  the row keys (<code>null</code> not permitted).
419     * @param columnKeys  the column keys (<code>null</code> not permitted).
420     * @param data  the data.
421     *
422     * @return The dataset.
423     */
424    public static CategoryDataset createCategoryDataset(Comparable[] rowKeys,
425            Comparable[] columnKeys, double[][] data) {
426
427        ParamChecks.nullNotPermitted(rowKeys, "rowKeys");
428        ParamChecks.nullNotPermitted(columnKeys, "columnKeys");
429        if (ArrayUtilities.hasDuplicateItems(rowKeys)) {
430            throw new IllegalArgumentException("Duplicate items in 'rowKeys'.");
431        }
432        if (ArrayUtilities.hasDuplicateItems(columnKeys)) {
433            throw new IllegalArgumentException(
434                    "Duplicate items in 'columnKeys'.");
435        }
436        if (rowKeys.length != data.length) {
437            throw new IllegalArgumentException(
438                "The number of row keys does not match the number of rows in "
439                + "the data array.");
440        }
441        int columnCount = 0;
442        for (int r = 0; r < data.length; r++) {
443            columnCount = Math.max(columnCount, data[r].length);
444        }
445        if (columnKeys.length != columnCount) {
446            throw new IllegalArgumentException(
447                "The number of column keys does not match the number of "
448                + "columns in the data array.");
449        }
450
451        // now do the work...
452        DefaultCategoryDataset result = new DefaultCategoryDataset();
453        for (int r = 0; r < data.length; r++) {
454            Comparable rowKey = rowKeys[r];
455            for (int c = 0; c < data[r].length; c++) {
456                Comparable columnKey = columnKeys[c];
457                result.addValue(new Double(data[r][c]), rowKey, columnKey);
458            }
459        }
460        return result;
461
462    }
463
464    /**
465     * Creates a {@link CategoryDataset} by copying the data from the supplied
466     * {@link KeyedValues} instance.
467     *
468     * @param rowKey  the row key (<code>null</code> not permitted).
469     * @param rowData  the row data (<code>null</code> not permitted).
470     *
471     * @return A dataset.
472     */
473    public static CategoryDataset createCategoryDataset(Comparable rowKey,
474            KeyedValues rowData) {
475
476        ParamChecks.nullNotPermitted(rowKey, "rowKey");
477        ParamChecks.nullNotPermitted(rowData, "rowData");
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 &gt; 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        // defer argument checking
503        XYSeries series = sampleFunction2DToSeries(f, start, end, samples,
504                seriesKey);
505        XYSeriesCollection collection = new XYSeriesCollection(series);
506        return collection;
507    }
508
509    /**
510     * Creates an {@link XYSeries} by sampling the specified function over a
511     * fixed range.
512     *
513     * @param f  the function (<code>null</code> not permitted).
514     * @param start  the start value for the range.
515     * @param end  the end value for the range.
516     * @param samples  the number of sample points (must be &gt; 1).
517     * @param seriesKey  the key to give the resulting series
518     *                   (<code>null</code> not permitted).
519     *
520     * @return A series.
521     *
522     * @since 1.0.13
523     */
524    public static XYSeries sampleFunction2DToSeries(Function2D f,
525            double start, double end, int samples, Comparable seriesKey) {
526
527        ParamChecks.nullNotPermitted(f, "f");
528        ParamChecks.nullNotPermitted(seriesKey, "seriesKey");
529        if (start >= end) {
530            throw new IllegalArgumentException("Requires 'start' < 'end'.");
531        }
532        if (samples < 2) {
533            throw new IllegalArgumentException("Requires 'samples' > 1");
534        }
535
536        XYSeries series = new XYSeries(seriesKey);
537        double step = (end - start) / (samples - 1);
538        for (int i = 0; i < samples; i++) {
539            double x = start + (step * i);
540            series.add(x, f.getValue(x));
541        }
542        return series;
543    }
544
545    /**
546     * Returns <code>true</code> if the dataset is empty (or <code>null</code>),
547     * and <code>false</code> otherwise.
548     *
549     * @param dataset  the dataset (<code>null</code> permitted).
550     *
551     * @return A boolean.
552     */
553    public static boolean isEmptyOrNull(PieDataset dataset) {
554
555        if (dataset == null) {
556            return true;
557        }
558
559        int itemCount = dataset.getItemCount();
560        if (itemCount == 0) {
561            return true;
562        }
563
564        for (int item = 0; item < itemCount; item++) {
565            Number y = dataset.getValue(item);
566            if (y != null) {
567                double yy = y.doubleValue();
568                if (yy > 0.0) {
569                    return false;
570                }
571            }
572        }
573
574        return true;
575
576    }
577
578    /**
579     * Returns <code>true</code> if the dataset is empty (or <code>null</code>),
580     * and <code>false</code> otherwise.
581     *
582     * @param dataset  the dataset (<code>null</code> permitted).
583     *
584     * @return A boolean.
585     */
586    public static boolean isEmptyOrNull(CategoryDataset dataset) {
587
588        if (dataset == null) {
589            return true;
590        }
591
592        int rowCount = dataset.getRowCount();
593        int columnCount = dataset.getColumnCount();
594        if (rowCount == 0 || columnCount == 0) {
595            return true;
596        }
597
598        for (int r = 0; r < rowCount; r++) {
599            for (int c = 0; c < columnCount; c++) {
600                if (dataset.getValue(r, c) != null) {
601                    return false;
602                }
603
604            }
605        }
606
607        return true;
608
609    }
610
611    /**
612     * Returns <code>true</code> if the dataset is empty (or <code>null</code>),
613     * and <code>false</code> otherwise.
614     *
615     * @param dataset  the dataset (<code>null</code> permitted).
616     *
617     * @return A boolean.
618     */
619    public static boolean isEmptyOrNull(XYDataset dataset) {
620        if (dataset != null) {
621            for (int s = 0; s < dataset.getSeriesCount(); s++) {
622                if (dataset.getItemCount(s) > 0) {
623                    return false;
624                }
625            }
626        }
627        return true;
628    }
629
630    /**
631     * Returns the range of values in the domain (x-values) of a dataset.
632     *
633     * @param dataset  the dataset (<code>null</code> not permitted).
634     *
635     * @return The range of values (possibly <code>null</code>).
636     */
637    public static Range findDomainBounds(XYDataset dataset) {
638        return findDomainBounds(dataset, true);
639    }
640
641    /**
642     * Returns the range of values in the domain (x-values) of a dataset.
643     *
644     * @param dataset  the dataset (<code>null</code> not permitted).
645     * @param includeInterval  determines whether or not the x-interval is taken
646     *                         into account (only applies if the dataset is an
647     *                         {@link IntervalXYDataset}).
648     *
649     * @return The range of values (possibly <code>null</code>).
650     */
651    public static Range findDomainBounds(XYDataset dataset,
652            boolean includeInterval) {
653
654        ParamChecks.nullNotPermitted(dataset, "dataset");
655
656        Range result;
657        // if the dataset implements DomainInfo, life is easier
658        if (dataset instanceof DomainInfo) {
659            DomainInfo info = (DomainInfo) dataset;
660            result = info.getDomainBounds(includeInterval);
661        }
662        else {
663            result = iterateDomainBounds(dataset, includeInterval);
664        }
665        return result;
666
667    }
668
669    /**
670     * Returns the bounds of the x-values in the specified <code>dataset</code>
671     * taking into account only the visible series and including any x-interval
672     * if requested.
673     *
674     * @param dataset  the dataset (<code>null</code> not permitted).
675     * @param visibleSeriesKeys  the visible series keys (<code>null</code>
676     *     not permitted).
677     * @param includeInterval  include the x-interval (if any)?
678     *
679     * @return The bounds (or <code>null</code> if the dataset contains no
680     *     values.
681     *
682     * @since 1.0.13
683     */
684    public static Range findDomainBounds(XYDataset dataset,
685            List visibleSeriesKeys, boolean includeInterval) {
686        
687        ParamChecks.nullNotPermitted(dataset, "dataset");
688        Range result;
689        if (dataset instanceof XYDomainInfo) {
690            XYDomainInfo info = (XYDomainInfo) dataset;
691            result = info.getDomainBounds(visibleSeriesKeys, includeInterval);
692        }
693        else {
694            result = iterateToFindDomainBounds(dataset, visibleSeriesKeys,
695                    includeInterval);
696        }
697        return result;
698    }
699
700    /**
701     * Iterates over the items in an {@link XYDataset} to find
702     * the range of x-values.  If the dataset is an instance of
703     * {@link IntervalXYDataset}, the starting and ending x-values
704     * will be used for the bounds calculation.
705     *
706     * @param dataset  the dataset (<code>null</code> not permitted).
707     *
708     * @return The range (possibly <code>null</code>).
709     */
710    public static Range iterateDomainBounds(XYDataset dataset) {
711        return iterateDomainBounds(dataset, true);
712    }
713
714    /**
715     * Iterates over the items in an {@link XYDataset} to find
716     * the range of x-values.
717     *
718     * @param dataset  the dataset (<code>null</code> not permitted).
719     * @param includeInterval  a flag that determines, for an
720     *          {@link IntervalXYDataset}, whether the x-interval or just the
721     *          x-value is used to determine the overall range.
722     *
723     * @return The range (possibly <code>null</code>).
724     */
725    public static Range iterateDomainBounds(XYDataset dataset,
726            boolean includeInterval) {
727        ParamChecks.nullNotPermitted(dataset, "dataset");
728        double minimum = Double.POSITIVE_INFINITY;
729        double maximum = Double.NEGATIVE_INFINITY;
730        int seriesCount = dataset.getSeriesCount();
731        double lvalue, uvalue;
732        if (includeInterval && dataset instanceof IntervalXYDataset) {
733            IntervalXYDataset intervalXYData = (IntervalXYDataset) dataset;
734            for (int series = 0; series < seriesCount; series++) {
735                int itemCount = dataset.getItemCount(series);
736                for (int item = 0; item < itemCount; item++) {
737                    double value = intervalXYData.getXValue(series, item);
738                    lvalue = intervalXYData.getStartXValue(series, item);
739                    uvalue = intervalXYData.getEndXValue(series, item);
740                    if (!Double.isNaN(value)) {
741                        minimum = Math.min(minimum, value);
742                        maximum = Math.max(maximum, value);
743                    }
744                    if (!Double.isNaN(lvalue)) {
745                        minimum = Math.min(minimum, lvalue);
746                        maximum = Math.max(maximum, lvalue);
747                    }
748                    if (!Double.isNaN(uvalue)) {
749                        minimum = Math.min(minimum, uvalue);
750                        maximum = Math.max(maximum, uvalue);
751                    }
752                }
753            }
754        }
755        else {
756            for (int series = 0; series < seriesCount; series++) {
757                int itemCount = dataset.getItemCount(series);
758                for (int item = 0; item < itemCount; item++) {
759                    lvalue = dataset.getXValue(series, item);
760                    uvalue = lvalue;
761                    if (!Double.isNaN(lvalue)) {
762                        minimum = Math.min(minimum, lvalue);
763                        maximum = Math.max(maximum, uvalue);
764                    }
765                }
766            }
767        }
768        if (minimum > maximum) {
769            return null;
770        }
771        else {
772            return new Range(minimum, maximum);
773        }
774    }
775
776    /**
777     * Returns the range of values in the range for the dataset.
778     *
779     * @param dataset  the dataset (<code>null</code> not permitted).
780     *
781     * @return The range (possibly <code>null</code>).
782     */
783    public static Range findRangeBounds(CategoryDataset dataset) {
784        return findRangeBounds(dataset, true);
785    }
786
787    /**
788     * Returns the range of values in the range for the dataset.
789     *
790     * @param dataset  the dataset (<code>null</code> not permitted).
791     * @param includeInterval  a flag that determines whether or not the
792     *                         y-interval is taken into account.
793     *
794     * @return The range (possibly <code>null</code>).
795     */
796    public static Range findRangeBounds(CategoryDataset dataset,
797            boolean includeInterval) {
798        ParamChecks.nullNotPermitted(dataset, "dataset");
799        Range result;
800        if (dataset instanceof RangeInfo) {
801            RangeInfo info = (RangeInfo) dataset;
802            result = info.getRangeBounds(includeInterval);
803        }
804        else {
805            result = iterateRangeBounds(dataset, includeInterval);
806        }
807        return result;
808    }
809
810    /**
811     * Finds the bounds of the y-values in the specified dataset, including
812     * only those series that are listed in visibleSeriesKeys.
813     *
814     * @param dataset  the dataset (<code>null</code> not permitted).
815     * @param visibleSeriesKeys  the keys for the visible series
816     *     (<code>null</code> not permitted).
817     * @param includeInterval  include the y-interval (if the dataset has a
818     *     y-interval).
819     *
820     * @return The data bounds.
821     *
822     * @since 1.0.13
823     */
824    public static Range findRangeBounds(CategoryDataset dataset,
825            List visibleSeriesKeys, boolean includeInterval) {
826        ParamChecks.nullNotPermitted(dataset, "dataset");
827        Range result;
828        if (dataset instanceof CategoryRangeInfo) {
829            CategoryRangeInfo info = (CategoryRangeInfo) dataset;
830            result = info.getRangeBounds(visibleSeriesKeys, includeInterval);
831        }
832        else {
833            result = iterateToFindRangeBounds(dataset, visibleSeriesKeys,
834                    includeInterval);
835        }
836        return result;
837    }
838
839    /**
840     * Returns the range of values in the range for the dataset.  This method
841     * is the partner for the {@link #findDomainBounds(XYDataset)} method.
842     *
843     * @param dataset  the dataset (<code>null</code> not permitted).
844     *
845     * @return The range (possibly <code>null</code>).
846     */
847    public static Range findRangeBounds(XYDataset dataset) {
848        return findRangeBounds(dataset, true);
849    }
850
851    /**
852     * Returns the range of values in the range for the dataset.  This method
853     * is the partner for the {@link #findDomainBounds(XYDataset, boolean)}
854     * method.
855     *
856     * @param dataset  the dataset (<code>null</code> not permitted).
857     * @param includeInterval  a flag that determines whether or not the
858     *                         y-interval is taken into account.
859     *
860     * @return The range (possibly <code>null</code>).
861     */
862    public static Range findRangeBounds(XYDataset dataset,
863            boolean includeInterval) {
864        ParamChecks.nullNotPermitted(dataset, "dataset");
865        Range result;
866        if (dataset instanceof RangeInfo) {
867            RangeInfo info = (RangeInfo) dataset;
868            result = info.getRangeBounds(includeInterval);
869        }
870        else {
871            result = iterateRangeBounds(dataset, includeInterval);
872        }
873        return result;
874    }
875
876    /**
877     * Finds the bounds of the y-values in the specified dataset, including
878     * only those series that are listed in visibleSeriesKeys, and those items
879     * whose x-values fall within the specified range.
880     *
881     * @param dataset  the dataset (<code>null</code> not permitted).
882     * @param visibleSeriesKeys  the keys for the visible series
883     *     (<code>null</code> not permitted).
884     * @param xRange  the x-range (<code>null</code> not permitted).
885     * @param includeInterval  include the y-interval (if the dataset has a
886     *     y-interval).
887     *
888     * @return The data bounds.
889     * 
890     * @since 1.0.13
891     */
892    public static Range findRangeBounds(XYDataset dataset,
893            List visibleSeriesKeys, Range xRange, boolean includeInterval) {
894        ParamChecks.nullNotPermitted(dataset, "dataset");
895        Range result;
896        if (dataset instanceof XYRangeInfo) {
897            XYRangeInfo info = (XYRangeInfo) dataset;
898            result = info.getRangeBounds(visibleSeriesKeys, xRange,
899                    includeInterval);
900        }
901        else {
902            result = iterateToFindRangeBounds(dataset, visibleSeriesKeys,
903                    xRange, includeInterval);
904        }
905        return result;
906    }
907
908    /**
909     * Iterates over the data item of the category dataset to find
910     * the range bounds.
911     *
912     * @param dataset  the dataset (<code>null</code> not permitted).
913     * @param includeInterval  a flag that determines whether or not the
914     *                         y-interval is taken into account.
915     *
916     * @return The range (possibly <code>null</code>).
917     *
918     * @deprecated As of 1.0.10, use
919     *         {@link #iterateRangeBounds(CategoryDataset, boolean)}.
920     */
921    public static Range iterateCategoryRangeBounds(CategoryDataset dataset,
922            boolean includeInterval) {
923        return iterateRangeBounds(dataset, includeInterval);
924    }
925
926    /**
927     * Iterates over the data item of the category dataset to find
928     * the range bounds.
929     *
930     * @param dataset  the dataset (<code>null</code> not permitted).
931     *
932     * @return The range (possibly <code>null</code>).
933     *
934     * @since 1.0.10
935     */
936    public static Range iterateRangeBounds(CategoryDataset dataset) {
937        return iterateRangeBounds(dataset, true);
938    }
939
940    /**
941     * Iterates over the data item of the category dataset to find
942     * the range bounds.
943     *
944     * @param dataset  the dataset (<code>null</code> not permitted).
945     * @param includeInterval  a flag that determines whether or not the
946     *                         y-interval is taken into account.
947     *
948     * @return The range (possibly <code>null</code>).
949     *
950     * @since 1.0.10
951     */
952    public static Range iterateRangeBounds(CategoryDataset dataset,
953            boolean includeInterval) {
954        double minimum = Double.POSITIVE_INFINITY;
955        double maximum = Double.NEGATIVE_INFINITY;
956        int rowCount = dataset.getRowCount();
957        int columnCount = dataset.getColumnCount();
958        if (includeInterval && dataset instanceof IntervalCategoryDataset) {
959            // handle the special case where the dataset has y-intervals that
960            // we want to measure
961            IntervalCategoryDataset icd = (IntervalCategoryDataset) dataset;
962            Number value, lvalue, uvalue;
963            for (int row = 0; row < rowCount; row++) {
964                for (int column = 0; column < columnCount; column++) {
965                    value = icd.getValue(row, column);
966                    double v;
967                    if ((value != null)
968                            && !Double.isNaN(v = value.doubleValue())) {
969                        minimum = Math.min(v, minimum);
970                        maximum = Math.max(v, maximum);
971                    }
972                    lvalue = icd.getStartValue(row, column);
973                    if (lvalue != null
974                            && !Double.isNaN(v = lvalue.doubleValue())) {
975                        minimum = Math.min(v, minimum);
976                        maximum = Math.max(v, maximum);
977                    }
978                    uvalue = icd.getEndValue(row, column);
979                    if (uvalue != null
980                            && !Double.isNaN(v = uvalue.doubleValue())) {
981                        minimum = Math.min(v, minimum);
982                        maximum = Math.max(v, maximum);
983                    }
984                }
985            }
986        }
987        else {
988            // handle the standard case (plain CategoryDataset)
989            for (int row = 0; row < rowCount; row++) {
990                for (int column = 0; column < columnCount; column++) {
991                    Number value = dataset.getValue(row, column);
992                    if (value != null) {
993                        double v = value.doubleValue();
994                        if (!Double.isNaN(v)) {
995                            minimum = Math.min(minimum, v);
996                            maximum = Math.max(maximum, v);
997                        }
998                    }
999                }
1000            }
1001        }
1002        if (minimum == Double.POSITIVE_INFINITY) {
1003            return null;
1004        }
1005        else {
1006            return new Range(minimum, maximum);
1007        }
1008    }
1009
1010    /**
1011     * Iterates over the data item of the category dataset to find
1012     * the range bounds.
1013     *
1014     * @param dataset  the dataset (<code>null</code> not permitted).
1015     * @param includeInterval  a flag that determines whether or not the
1016     *                         y-interval is taken into account.
1017     * @param visibleSeriesKeys  the visible series keys.
1018     *
1019     * @return The range (possibly <code>null</code>).
1020     *
1021     * @since 1.0.13
1022     */
1023    public static Range iterateToFindRangeBounds(CategoryDataset dataset,
1024            List visibleSeriesKeys, boolean includeInterval) {
1025
1026        ParamChecks.nullNotPermitted(dataset, "dataset");
1027        ParamChecks.nullNotPermitted(visibleSeriesKeys, "visibleSeriesKeys");
1028
1029        double minimum = Double.POSITIVE_INFINITY;
1030        double maximum = Double.NEGATIVE_INFINITY;
1031        int columnCount = dataset.getColumnCount();
1032        if (includeInterval
1033                && dataset instanceof BoxAndWhiskerCategoryDataset) {
1034            // handle special case of BoxAndWhiskerDataset
1035            BoxAndWhiskerCategoryDataset bx
1036                    = (BoxAndWhiskerCategoryDataset) dataset;
1037            Iterator iterator = visibleSeriesKeys.iterator();
1038            while (iterator.hasNext()) {
1039                Comparable seriesKey = (Comparable) iterator.next();
1040                int series = dataset.getRowIndex(seriesKey);
1041                int itemCount = dataset.getColumnCount();
1042                for (int item = 0; item < itemCount; item++) {
1043                    Number lvalue = bx.getMinRegularValue(series, item);
1044                    if (lvalue == null) {
1045                        lvalue = bx.getValue(series, item);
1046                    }
1047                    Number uvalue = bx.getMaxRegularValue(series, item);
1048                    if (uvalue == null) {
1049                        uvalue = bx.getValue(series, item);
1050                    }
1051                    if (lvalue != null) {
1052                        minimum = Math.min(minimum, lvalue.doubleValue());
1053                    }
1054                    if (uvalue != null) {
1055                        maximum = Math.max(maximum, uvalue.doubleValue());
1056                    }
1057                }
1058            }
1059        }
1060        else if (includeInterval
1061                && dataset instanceof IntervalCategoryDataset) {
1062            // handle the special case where the dataset has y-intervals that
1063            // we want to measure
1064            IntervalCategoryDataset icd = (IntervalCategoryDataset) dataset;
1065            Number lvalue, uvalue;
1066            Iterator iterator = visibleSeriesKeys.iterator();
1067            while (iterator.hasNext()) {
1068                Comparable seriesKey = (Comparable) iterator.next();
1069                int series = dataset.getRowIndex(seriesKey);
1070                for (int column = 0; column < columnCount; column++) {
1071                    lvalue = icd.getStartValue(series, column);
1072                    uvalue = icd.getEndValue(series, column);
1073                    if (lvalue != null && !Double.isNaN(lvalue.doubleValue())) {
1074                        minimum = Math.min(minimum, lvalue.doubleValue());
1075                    }
1076                    if (uvalue != null && !Double.isNaN(uvalue.doubleValue())) {
1077                        maximum = Math.max(maximum, uvalue.doubleValue());
1078                    }
1079                }
1080            }
1081        }
1082        else if (includeInterval
1083                && dataset instanceof MultiValueCategoryDataset) {
1084            // handle the special case where the dataset has y-intervals that
1085            // we want to measure
1086            MultiValueCategoryDataset mvcd
1087                    = (MultiValueCategoryDataset) dataset;
1088            Iterator iterator = visibleSeriesKeys.iterator();
1089            while (iterator.hasNext()) {
1090                Comparable seriesKey = (Comparable) iterator.next();
1091                int series = dataset.getRowIndex(seriesKey);
1092                for (int column = 0; column < columnCount; column++) {
1093                    List values = mvcd.getValues(series, column);
1094                    Iterator valueIterator = values.iterator();
1095                    while (valueIterator.hasNext()) {
1096                        Object o = valueIterator.next();
1097                        if (o instanceof Number){
1098                            double v = ((Number) o).doubleValue();
1099                            if (!Double.isNaN(v)){
1100                                minimum = Math.min(minimum, v);
1101                                maximum = Math.max(maximum, v);
1102                            }
1103                        }
1104                    }
1105               }
1106            }
1107        }
1108        else if (includeInterval 
1109                && dataset instanceof StatisticalCategoryDataset) {
1110            // handle the special case where the dataset has y-intervals that
1111            // we want to measure
1112            StatisticalCategoryDataset scd
1113                    = (StatisticalCategoryDataset) dataset;
1114            Iterator iterator = visibleSeriesKeys.iterator();
1115            while (iterator.hasNext()) {
1116                Comparable seriesKey = (Comparable) iterator.next();
1117                int series = dataset.getRowIndex(seriesKey);
1118                for (int column = 0; column < columnCount; column++) {
1119                    Number meanN = scd.getMeanValue(series, column);
1120                    if (meanN != null) {
1121                        double std = 0.0;
1122                        Number stdN = scd.getStdDevValue(series, column);
1123                        if (stdN != null) {
1124                            std = stdN.doubleValue();
1125                            if (Double.isNaN(std)) {
1126                                std = 0.0;
1127                            }
1128                        }
1129                        double mean = meanN.doubleValue();
1130                        if (!Double.isNaN(mean)) {
1131                            minimum = Math.min(minimum, mean - std);
1132                            maximum = Math.max(maximum, mean + std);
1133                        }
1134                    }
1135                }
1136            }
1137        }
1138        else {
1139            // handle the standard case (plain CategoryDataset)
1140            Iterator iterator = visibleSeriesKeys.iterator();
1141            while (iterator.hasNext()) {
1142                Comparable seriesKey = (Comparable) iterator.next();
1143                int series = dataset.getRowIndex(seriesKey);
1144                for (int column = 0; column < columnCount; column++) {
1145                    Number value = dataset.getValue(series, column);
1146                    if (value != null) {
1147                        double v = value.doubleValue();
1148                        if (!Double.isNaN(v)) {
1149                            minimum = Math.min(minimum, v);
1150                            maximum = Math.max(maximum, v);
1151                        }
1152                    }
1153                }
1154            }
1155        }
1156        if (minimum == Double.POSITIVE_INFINITY) {
1157            return null;
1158        }
1159        else {
1160            return new Range(minimum, maximum);
1161        }
1162    }
1163
1164    /**
1165     * Iterates over the data item of the xy dataset to find
1166     * the range bounds.
1167     *
1168     * @param dataset  the dataset (<code>null</code> not permitted).
1169     *
1170     * @return The range (possibly <code>null</code>).
1171     *
1172     * @deprecated As of 1.0.10, use {@link #iterateRangeBounds(XYDataset)}.
1173     */
1174    public static Range iterateXYRangeBounds(XYDataset dataset) {
1175        return iterateRangeBounds(dataset);
1176    }
1177
1178    /**
1179     * Iterates over the data item of the xy dataset to find
1180     * the range bounds.
1181     *
1182     * @param dataset  the dataset (<code>null</code> not permitted).
1183     *
1184     * @return The range (possibly <code>null</code>).
1185     *
1186     * @since 1.0.10
1187     */
1188    public static Range iterateRangeBounds(XYDataset dataset) {
1189        return iterateRangeBounds(dataset, true);
1190    }
1191
1192    /**
1193     * Iterates over the data items of the xy dataset to find
1194     * the range bounds.
1195     *
1196     * @param dataset  the dataset (<code>null</code> not permitted).
1197     * @param includeInterval  a flag that determines, for an
1198     *          {@link IntervalXYDataset}, whether the y-interval or just the
1199     *          y-value is used to determine the overall range.
1200     *
1201     * @return The range (possibly <code>null</code>).
1202     *
1203     * @since 1.0.10
1204     */
1205    public static Range iterateRangeBounds(XYDataset dataset,
1206            boolean includeInterval) {
1207        double minimum = Double.POSITIVE_INFINITY;
1208        double maximum = Double.NEGATIVE_INFINITY;
1209        int seriesCount = dataset.getSeriesCount();
1210
1211        // handle three cases by dataset type
1212        if (includeInterval && dataset instanceof IntervalXYDataset) {
1213            // handle special case of IntervalXYDataset
1214            IntervalXYDataset ixyd = (IntervalXYDataset) dataset;
1215            for (int series = 0; series < seriesCount; series++) {
1216                int itemCount = dataset.getItemCount(series);
1217                for (int item = 0; item < itemCount; item++) {
1218                    double value = ixyd.getYValue(series, item);
1219                    double lvalue = ixyd.getStartYValue(series, item);
1220                    double uvalue = ixyd.getEndYValue(series, item);
1221                    if (!Double.isNaN(value)) {
1222                        minimum = Math.min(minimum, value);
1223                        maximum = Math.max(maximum, value);
1224                    }
1225                    if (!Double.isNaN(lvalue)) {
1226                        minimum = Math.min(minimum, lvalue);
1227                        maximum = Math.max(maximum, lvalue);
1228                    }
1229                    if (!Double.isNaN(uvalue)) {
1230                        minimum = Math.min(minimum, uvalue);
1231                        maximum = Math.max(maximum, uvalue);
1232                    }
1233                }
1234            }
1235        }
1236        else if (includeInterval && dataset instanceof OHLCDataset) {
1237            // handle special case of OHLCDataset
1238            OHLCDataset ohlc = (OHLCDataset) dataset;
1239            for (int series = 0; series < seriesCount; series++) {
1240                int itemCount = dataset.getItemCount(series);
1241                for (int item = 0; item < itemCount; item++) {
1242                    double lvalue = ohlc.getLowValue(series, item);
1243                    double uvalue = ohlc.getHighValue(series, item);
1244                    if (!Double.isNaN(lvalue)) {
1245                        minimum = Math.min(minimum, lvalue);
1246                    }
1247                    if (!Double.isNaN(uvalue)) {
1248                        maximum = Math.max(maximum, uvalue);
1249                    }
1250                }
1251            }
1252        }
1253        else {
1254            // standard case - plain XYDataset
1255            for (int series = 0; series < seriesCount; series++) {
1256                int itemCount = dataset.getItemCount(series);
1257                for (int item = 0; item < itemCount; item++) {
1258                    double value = dataset.getYValue(series, item);
1259                    if (!Double.isNaN(value)) {
1260                        minimum = Math.min(minimum, value);
1261                        maximum = Math.max(maximum, value);
1262                    }
1263                }
1264            }
1265        }
1266        if (minimum == Double.POSITIVE_INFINITY) {
1267            return null;
1268        }
1269        else {
1270            return new Range(minimum, maximum);
1271        }
1272    }
1273
1274    /**
1275     * Returns the range of values in the z-dimension for the dataset. This
1276     * method is the partner for the {@link #findRangeBounds(XYDataset)}
1277     * and {@link #findDomainBounds(XYDataset)} methods.
1278     *
1279     * @param dataset  the dataset (<code>null</code> not permitted).
1280     *
1281     * @return The range (possibly <code>null</code>).
1282     */
1283    public static Range findZBounds(XYZDataset dataset) {
1284        return findZBounds(dataset, true);
1285    }
1286
1287    /**
1288     * Returns the range of values in the z-dimension for the dataset.  This
1289     * method is the partner for the
1290     * {@link #findRangeBounds(XYDataset, boolean)} and
1291     * {@link #findDomainBounds(XYDataset, boolean)} methods.
1292     *
1293     * @param dataset  the dataset (<code>null</code> not permitted).
1294     * @param includeInterval  a flag that determines whether or not the
1295     *                         z-interval is taken into account.
1296     *
1297     * @return The range (possibly <code>null</code>).
1298     */
1299    public static Range findZBounds(XYZDataset dataset,
1300            boolean includeInterval) {
1301        ParamChecks.nullNotPermitted(dataset, "dataset");
1302        Range result = iterateZBounds(dataset, includeInterval);
1303        return result;
1304    }
1305
1306    /**
1307     * Finds the bounds of the z-values in the specified dataset, including
1308     * only those series that are listed in visibleSeriesKeys, and those items
1309     * whose x-values fall within the specified range.
1310     *
1311     * @param dataset  the dataset (<code>null</code> not permitted).
1312     * @param visibleSeriesKeys  the keys for the visible series
1313     *     (<code>null</code> not permitted).
1314     * @param xRange  the x-range (<code>null</code> not permitted).
1315     * @param includeInterval  include the z-interval (if the dataset has a
1316     *     z-interval).
1317     *
1318     * @return The data bounds.
1319     */
1320    public static Range findZBounds(XYZDataset dataset,
1321            List visibleSeriesKeys, Range xRange, boolean includeInterval) {
1322        ParamChecks.nullNotPermitted(dataset, "dataset");
1323        Range result = iterateToFindZBounds(dataset, visibleSeriesKeys,
1324                    xRange, includeInterval);
1325        return result;
1326    }
1327
1328    /**
1329     * Iterates over the data item of the xyz dataset to find
1330     * the z-dimension bounds.
1331     *
1332     * @param dataset  the dataset (<code>null</code> not permitted).
1333     *
1334     * @return The range (possibly <code>null</code>).
1335     */
1336    public static Range iterateZBounds(XYZDataset dataset) {
1337        return iterateZBounds(dataset, true);
1338    }
1339
1340    /**
1341     * Iterates over the data items of the xyz dataset to find
1342     * the z-dimension bounds.
1343     *
1344     * @param dataset  the dataset (<code>null</code> not permitted).
1345     * @param includeInterval  include the z-interval (if the dataset has a
1346     *     z-interval.
1347     *
1348     * @return The range (possibly <code>null</code>).
1349     */
1350    public static Range iterateZBounds(XYZDataset dataset,
1351            boolean includeInterval) {
1352        double minimum = Double.POSITIVE_INFINITY;
1353        double maximum = Double.NEGATIVE_INFINITY;
1354        int seriesCount = dataset.getSeriesCount();
1355
1356        for (int series = 0; series < seriesCount; series++) {
1357            int itemCount = dataset.getItemCount(series);
1358            for (int item = 0; item < itemCount; item++) {
1359                double value = dataset.getZValue(series, item);
1360                if (!Double.isNaN(value)) {
1361                    minimum = Math.min(minimum, value);
1362                    maximum = Math.max(maximum, value);
1363                }
1364            }
1365        }
1366
1367        if (minimum == Double.POSITIVE_INFINITY) {
1368            return null;
1369        }
1370        else {
1371            return new Range(minimum, maximum);
1372        }
1373    }
1374
1375    /**
1376     * Returns the range of x-values in the specified dataset for the
1377     * data items belonging to the visible series.
1378     * 
1379     * @param dataset  the dataset (<code>null</code> not permitted).
1380     * @param visibleSeriesKeys  the visible series keys (<code>null</code> not
1381     *     permitted).
1382     * @param includeInterval  a flag that determines whether or not the
1383     *     y-interval for the dataset is included (this only applies if the
1384     *     dataset is an instance of IntervalXYDataset).
1385     * 
1386     * @return The x-range (possibly <code>null</code>).
1387     * 
1388     * @since 1.0.13
1389     */
1390    public static Range iterateToFindDomainBounds(XYDataset dataset,
1391            List visibleSeriesKeys, boolean includeInterval) {
1392        ParamChecks.nullNotPermitted(dataset, "dataset");
1393        ParamChecks.nullNotPermitted(visibleSeriesKeys, "visibleSeriesKeys");
1394
1395        double minimum = Double.POSITIVE_INFINITY;
1396        double maximum = Double.NEGATIVE_INFINITY;
1397
1398        if (includeInterval && dataset instanceof IntervalXYDataset) {
1399            // handle special case of IntervalXYDataset
1400            IntervalXYDataset ixyd = (IntervalXYDataset) dataset;
1401            Iterator iterator = visibleSeriesKeys.iterator();
1402            while (iterator.hasNext()) {
1403                Comparable seriesKey = (Comparable) iterator.next();
1404                int series = dataset.indexOf(seriesKey);
1405                int itemCount = dataset.getItemCount(series);
1406                for (int item = 0; item < itemCount; item++) {
1407                    double lvalue = ixyd.getStartXValue(series, item);
1408                    double uvalue = ixyd.getEndXValue(series, item);
1409                    if (!Double.isNaN(lvalue)) {
1410                        minimum = Math.min(minimum, lvalue);
1411                    }
1412                    if (!Double.isNaN(uvalue)) {
1413                        maximum = Math.max(maximum, uvalue);
1414                    }
1415                }
1416            }
1417        }
1418        else {
1419            // standard case - plain XYDataset
1420            Iterator iterator = visibleSeriesKeys.iterator();
1421            while (iterator.hasNext()) {
1422                Comparable seriesKey = (Comparable) iterator.next();
1423                int series = dataset.indexOf(seriesKey);
1424                int itemCount = dataset.getItemCount(series);
1425                for (int item = 0; item < itemCount; item++) {
1426                    double x = dataset.getXValue(series, item);
1427                    if (!Double.isNaN(x)) {
1428                        minimum = Math.min(minimum, x);
1429                        maximum = Math.max(maximum, x);
1430                    }
1431                }
1432            }
1433        }
1434
1435        if (minimum == Double.POSITIVE_INFINITY) {
1436            return null;
1437        }
1438        else {
1439            return new Range(minimum, maximum);
1440        }
1441    }
1442
1443    /**
1444     * Returns the range of y-values in the specified dataset for the
1445     * data items belonging to the visible series and with x-values in the
1446     * given range.
1447     *
1448     * @param dataset  the dataset (<code>null</code> not permitted).
1449     * @param visibleSeriesKeys  the visible series keys (<code>null</code> not
1450     *     permitted).
1451     * @param xRange  the x-range (<code>null</code> not permitted).
1452     * @param includeInterval  a flag that determines whether or not the
1453     *     y-interval for the dataset is included (this only applies if the
1454     *     dataset is an instance of IntervalXYDataset).
1455     *
1456     * @return The y-range (possibly <code>null</code>).
1457     *
1458     * @since 1.0.13
1459     */
1460    public static Range iterateToFindRangeBounds(XYDataset dataset,
1461            List visibleSeriesKeys, Range xRange, boolean includeInterval) {
1462
1463        ParamChecks.nullNotPermitted(dataset, "dataset");
1464        ParamChecks.nullNotPermitted(visibleSeriesKeys, "visibleSeriesKeys");
1465        ParamChecks.nullNotPermitted(xRange, "xRange");
1466
1467        double minimum = Double.POSITIVE_INFINITY;
1468        double maximum = Double.NEGATIVE_INFINITY;
1469
1470        // handle three cases by dataset type
1471        if (includeInterval && dataset instanceof OHLCDataset) {
1472            // handle special case of OHLCDataset
1473            OHLCDataset ohlc = (OHLCDataset) dataset;
1474            Iterator iterator = visibleSeriesKeys.iterator();
1475            while (iterator.hasNext()) {
1476                Comparable seriesKey = (Comparable) iterator.next();
1477                int series = dataset.indexOf(seriesKey);
1478                int itemCount = dataset.getItemCount(series);
1479                for (int item = 0; item < itemCount; item++) {
1480                    double x = ohlc.getXValue(series, item);
1481                    if (xRange.contains(x)) {
1482                        double lvalue = ohlc.getLowValue(series, item);
1483                        double uvalue = ohlc.getHighValue(series, item);
1484                        if (!Double.isNaN(lvalue)) {
1485                            minimum = Math.min(minimum, lvalue);
1486                        }
1487                        if (!Double.isNaN(uvalue)) {
1488                            maximum = Math.max(maximum, uvalue);
1489                        }
1490                    }
1491                }
1492            }
1493        }
1494        else if (includeInterval && dataset instanceof BoxAndWhiskerXYDataset) {
1495            // handle special case of BoxAndWhiskerXYDataset
1496            BoxAndWhiskerXYDataset bx = (BoxAndWhiskerXYDataset) dataset;
1497            Iterator iterator = visibleSeriesKeys.iterator();
1498            while (iterator.hasNext()) {
1499                Comparable seriesKey = (Comparable) iterator.next();
1500                int series = dataset.indexOf(seriesKey);
1501                int itemCount = dataset.getItemCount(series);
1502                for (int item = 0; item < itemCount; item++) {
1503                    double x = bx.getXValue(series, item);
1504                    if (xRange.contains(x)) {
1505                        Number lvalue = bx.getMinRegularValue(series, item);
1506                        Number uvalue = bx.getMaxRegularValue(series, item);
1507                        if (lvalue != null) {
1508                            minimum = Math.min(minimum, lvalue.doubleValue());
1509                        }
1510                        if (uvalue != null) {
1511                            maximum = Math.max(maximum, uvalue.doubleValue());
1512                        }
1513                    }
1514                }
1515            }
1516        }
1517        else if (includeInterval && dataset instanceof IntervalXYDataset) {
1518            // handle special case of IntervalXYDataset
1519            IntervalXYDataset ixyd = (IntervalXYDataset) dataset;
1520            Iterator iterator = visibleSeriesKeys.iterator();
1521            while (iterator.hasNext()) {
1522                Comparable seriesKey = (Comparable) iterator.next();
1523                int series = dataset.indexOf(seriesKey);
1524                int itemCount = dataset.getItemCount(series);
1525                for (int item = 0; item < itemCount; item++) {
1526                    double x = ixyd.getXValue(series, item);
1527                    if (xRange.contains(x)) {
1528                        double lvalue = ixyd.getStartYValue(series, item);
1529                        double uvalue = ixyd.getEndYValue(series, item);
1530                        if (!Double.isNaN(lvalue)) {
1531                            minimum = Math.min(minimum, lvalue);
1532                        }
1533                        if (!Double.isNaN(uvalue)) {
1534                            maximum = Math.max(maximum, uvalue);
1535                        }
1536                    }
1537                }
1538            }
1539        }
1540        else {
1541            // standard case - plain XYDataset
1542            Iterator iterator = visibleSeriesKeys.iterator();
1543            while (iterator.hasNext()) {
1544                Comparable seriesKey = (Comparable) iterator.next();
1545                int series = dataset.indexOf(seriesKey);
1546                int itemCount = dataset.getItemCount(series);
1547                for (int item = 0; item < itemCount; item++) {
1548                    double x = dataset.getXValue(series, item);
1549                    double y = dataset.getYValue(series, item);
1550                    if (xRange.contains(x)) {
1551                        if (!Double.isNaN(y)) {
1552                            minimum = Math.min(minimum, y);
1553                            maximum = Math.max(maximum, y);
1554                        }
1555                    }
1556                }
1557            }
1558        }
1559        if (minimum == Double.POSITIVE_INFINITY) {
1560            return null;
1561        }
1562        else {
1563            return new Range(minimum, maximum);
1564        }
1565    }
1566
1567    /**
1568     * Returns the range of z-values in the specified dataset for the
1569     * data items belonging to the visible series and with x-values in the
1570     * given range.
1571     *
1572     * @param dataset  the dataset (<code>null</code> not permitted).
1573     * @param visibleSeriesKeys  the visible series keys (<code>null</code> not
1574     *     permitted).
1575     * @param xRange  the x-range (<code>null</code> not permitted).
1576     * @param includeInterval  a flag that determines whether or not the
1577     *     z-interval for the dataset is included (this only applies if the
1578     *     dataset has an interval, which is currently not supported).
1579     *
1580     * @return The y-range (possibly <code>null</code>).
1581     */
1582    public static Range iterateToFindZBounds(XYZDataset dataset,
1583            List visibleSeriesKeys, Range xRange, boolean includeInterval) {
1584        ParamChecks.nullNotPermitted(dataset, "dataset");
1585        ParamChecks.nullNotPermitted(visibleSeriesKeys, "visibleSeriesKeys");
1586        ParamChecks.nullNotPermitted(xRange, "xRange");
1587    
1588        double minimum = Double.POSITIVE_INFINITY;
1589        double maximum = Double.NEGATIVE_INFINITY;
1590    
1591        Iterator iterator = visibleSeriesKeys.iterator();
1592        while (iterator.hasNext()) {
1593            Comparable seriesKey = (Comparable) iterator.next();
1594            int series = dataset.indexOf(seriesKey);
1595            int itemCount = dataset.getItemCount(series);
1596            for (int item = 0; item < itemCount; item++) {
1597                double x = dataset.getXValue(series, item);
1598                double z = dataset.getZValue(series, item);
1599                if (xRange.contains(x)) {
1600                    if (!Double.isNaN(z)) {
1601                        minimum = Math.min(minimum, z);
1602                        maximum = Math.max(maximum, z);
1603                    }
1604                }
1605            }
1606        }
1607
1608        if (minimum == Double.POSITIVE_INFINITY) {
1609            return null;
1610        }
1611        else {
1612            return new Range(minimum, maximum);
1613        }
1614    }
1615
1616    /**
1617     * Finds the minimum domain (or X) value for the specified dataset.  This
1618     * is easy if the dataset implements the {@link DomainInfo} interface (a
1619     * good idea if there is an efficient way to determine the minimum value).
1620     * Otherwise, it involves iterating over the entire data-set.
1621     * <p>
1622     * Returns <code>null</code> if all the data values in the dataset are
1623     * <code>null</code>.
1624     *
1625     * @param dataset  the dataset (<code>null</code> not permitted).
1626     *
1627     * @return The minimum value (possibly <code>null</code>).
1628     */
1629    public static Number findMinimumDomainValue(XYDataset dataset) {
1630        ParamChecks.nullNotPermitted(dataset, "dataset");
1631        Number result;
1632        // if the dataset implements DomainInfo, life is easy
1633        if (dataset instanceof DomainInfo) {
1634            DomainInfo info = (DomainInfo) dataset;
1635            return new Double(info.getDomainLowerBound(true));
1636        }
1637        else {
1638            double minimum = Double.POSITIVE_INFINITY;
1639            int seriesCount = dataset.getSeriesCount();
1640            for (int series = 0; series < seriesCount; series++) {
1641                int itemCount = dataset.getItemCount(series);
1642                for (int item = 0; item < itemCount; item++) {
1643
1644                    double value;
1645                    if (dataset instanceof IntervalXYDataset) {
1646                        IntervalXYDataset intervalXYData
1647                            = (IntervalXYDataset) dataset;
1648                        value = intervalXYData.getStartXValue(series, item);
1649                    }
1650                    else {
1651                        value = dataset.getXValue(series, item);
1652                    }
1653                    if (!Double.isNaN(value)) {
1654                        minimum = Math.min(minimum, value);
1655                    }
1656
1657                }
1658            }
1659            if (minimum == Double.POSITIVE_INFINITY) {
1660                result = null;
1661            }
1662            else {
1663                result = new Double(minimum);
1664            }
1665        }
1666
1667        return result;
1668    }
1669
1670    /**
1671     * Returns the maximum domain value for the specified dataset.  This is
1672     * easy if the dataset implements the {@link DomainInfo} interface (a good
1673     * idea if there is an efficient way to determine the maximum value).
1674     * Otherwise, it involves iterating over the entire data-set.  Returns
1675     * <code>null</code> if all the data values in the dataset are
1676     * <code>null</code>.
1677     *
1678     * @param dataset  the dataset (<code>null</code> not permitted).
1679     *
1680     * @return The maximum value (possibly <code>null</code>).
1681     */
1682    public static Number findMaximumDomainValue(XYDataset dataset) {
1683        ParamChecks.nullNotPermitted(dataset, "dataset");
1684        Number result;
1685        // if the dataset implements DomainInfo, life is easy
1686        if (dataset instanceof DomainInfo) {
1687            DomainInfo info = (DomainInfo) dataset;
1688            return new Double(info.getDomainUpperBound(true));
1689        }
1690
1691        // hasn't implemented DomainInfo, so iterate...
1692        else {
1693            double maximum = Double.NEGATIVE_INFINITY;
1694            int seriesCount = dataset.getSeriesCount();
1695            for (int series = 0; series < seriesCount; series++) {
1696                int itemCount = dataset.getItemCount(series);
1697                for (int item = 0; item < itemCount; item++) {
1698
1699                    double value;
1700                    if (dataset instanceof IntervalXYDataset) {
1701                        IntervalXYDataset intervalXYData
1702                            = (IntervalXYDataset) dataset;
1703                        value = intervalXYData.getEndXValue(series, item);
1704                    }
1705                    else {
1706                        value = dataset.getXValue(series, item);
1707                    }
1708                    if (!Double.isNaN(value)) {
1709                        maximum = Math.max(maximum, value);
1710                    }
1711                }
1712            }
1713            if (maximum == Double.NEGATIVE_INFINITY) {
1714                result = null;
1715            }
1716            else {
1717                result = new Double(maximum);
1718            }
1719
1720        }
1721
1722        return result;
1723    }
1724
1725    /**
1726     * Returns the minimum range value for the specified dataset.  This is
1727     * easy if the dataset implements the {@link RangeInfo} interface (a good
1728     * idea if there is an efficient way to determine the minimum value).
1729     * Otherwise, it involves iterating over the entire data-set.  Returns
1730     * <code>null</code> if all the data values in the dataset are
1731     * <code>null</code>.
1732     *
1733     * @param dataset  the dataset (<code>null</code> not permitted).
1734     *
1735     * @return The minimum value (possibly <code>null</code>).
1736     */
1737    public static Number findMinimumRangeValue(CategoryDataset dataset) {
1738        ParamChecks.nullNotPermitted(dataset, "dataset");
1739        if (dataset instanceof RangeInfo) {
1740            RangeInfo info = (RangeInfo) dataset;
1741            return new Double(info.getRangeLowerBound(true));
1742        }
1743
1744        // hasn't implemented RangeInfo, so we'll have to iterate...
1745        else {
1746            double minimum = Double.POSITIVE_INFINITY;
1747            int seriesCount = dataset.getRowCount();
1748            int itemCount = dataset.getColumnCount();
1749            for (int series = 0; series < seriesCount; series++) {
1750                for (int item = 0; item < itemCount; item++) {
1751                    Number value;
1752                    if (dataset instanceof IntervalCategoryDataset) {
1753                        IntervalCategoryDataset icd
1754                                = (IntervalCategoryDataset) dataset;
1755                        value = icd.getStartValue(series, item);
1756                    }
1757                    else {
1758                        value = dataset.getValue(series, item);
1759                    }
1760                    if (value != null) {
1761                        minimum = Math.min(minimum, value.doubleValue());
1762                    }
1763                }
1764            }
1765            if (minimum == Double.POSITIVE_INFINITY) {
1766                return null;
1767            }
1768            else {
1769                return new Double(minimum);
1770            }
1771
1772        }
1773
1774    }
1775
1776    /**
1777     * Returns the minimum range value for the specified dataset.  This is
1778     * easy if the dataset implements the {@link RangeInfo} interface (a good
1779     * idea if there is an efficient way to determine the minimum value).
1780     * Otherwise, it involves iterating over the entire data-set.  Returns
1781     * <code>null</code> if all the data values in the dataset are
1782     * <code>null</code>.
1783     *
1784     * @param dataset  the dataset (<code>null</code> not permitted).
1785     *
1786     * @return The minimum value (possibly <code>null</code>).
1787     */
1788    public static Number findMinimumRangeValue(XYDataset dataset) {
1789        ParamChecks.nullNotPermitted(dataset, "dataset");
1790
1791        // work out the minimum value...
1792        if (dataset instanceof RangeInfo) {
1793            RangeInfo info = (RangeInfo) dataset;
1794            return new Double(info.getRangeLowerBound(true));
1795        }
1796
1797        // hasn't implemented RangeInfo, so we'll have to iterate...
1798        else {
1799            double minimum = Double.POSITIVE_INFINITY;
1800            int seriesCount = dataset.getSeriesCount();
1801            for (int series = 0; series < seriesCount; series++) {
1802                int itemCount = dataset.getItemCount(series);
1803                for (int item = 0; item < itemCount; item++) {
1804
1805                    double value;
1806                    if (dataset instanceof IntervalXYDataset) {
1807                        IntervalXYDataset intervalXYData
1808                                = (IntervalXYDataset) dataset;
1809                        value = intervalXYData.getStartYValue(series, item);
1810                    }
1811                    else if (dataset instanceof OHLCDataset) {
1812                        OHLCDataset highLowData = (OHLCDataset) dataset;
1813                        value = highLowData.getLowValue(series, item);
1814                    }
1815                    else {
1816                        value = dataset.getYValue(series, item);
1817                    }
1818                    if (!Double.isNaN(value)) {
1819                        minimum = Math.min(minimum, value);
1820                    }
1821
1822                }
1823            }
1824            if (minimum == Double.POSITIVE_INFINITY) {
1825                return null;
1826            }
1827            else {
1828                return new Double(minimum);
1829            }
1830
1831        }
1832
1833    }
1834
1835    /**
1836     * Returns the maximum range value for the specified dataset.  This is easy
1837     * if the dataset implements the {@link RangeInfo} interface (a good idea
1838     * if there is an efficient way to determine the maximum value).
1839     * Otherwise, it involves iterating over the entire data-set.  Returns
1840     * <code>null</code> if all the data values are <code>null</code>.
1841     *
1842     * @param dataset  the dataset (<code>null</code> not permitted).
1843     *
1844     * @return The maximum value (possibly <code>null</code>).
1845     */
1846    public static Number findMaximumRangeValue(CategoryDataset dataset) {
1847
1848        ParamChecks.nullNotPermitted(dataset, "dataset");
1849
1850        // work out the minimum value...
1851        if (dataset instanceof RangeInfo) {
1852            RangeInfo info = (RangeInfo) dataset;
1853            return new Double(info.getRangeUpperBound(true));
1854        }
1855
1856        // hasn't implemented RangeInfo, so we'll have to iterate...
1857        else {
1858
1859            double maximum = Double.NEGATIVE_INFINITY;
1860            int seriesCount = dataset.getRowCount();
1861            int itemCount = dataset.getColumnCount();
1862            for (int series = 0; series < seriesCount; series++) {
1863                for (int item = 0; item < itemCount; item++) {
1864                    Number value;
1865                    if (dataset instanceof IntervalCategoryDataset) {
1866                        IntervalCategoryDataset icd
1867                            = (IntervalCategoryDataset) dataset;
1868                        value = icd.getEndValue(series, item);
1869                    }
1870                    else {
1871                        value = dataset.getValue(series, item);
1872                    }
1873                    if (value != null) {
1874                        maximum = Math.max(maximum, value.doubleValue());
1875                    }
1876                }
1877            }
1878            if (maximum == Double.NEGATIVE_INFINITY) {
1879                return null;
1880            }
1881            else {
1882                return new Double(maximum);
1883            }
1884
1885        }
1886
1887    }
1888
1889    /**
1890     * Returns the maximum range value for the specified dataset.  This is
1891     * easy if the dataset implements the {@link RangeInfo} interface (a good
1892     * idea if there is an efficient way to determine the maximum value).
1893     * Otherwise, it involves iterating over the entire data-set.  Returns
1894     * <code>null</code> if all the data values are <code>null</code>.
1895     *
1896     * @param dataset  the dataset (<code>null</code> not permitted).
1897     *
1898     * @return The maximum value (possibly <code>null</code>).
1899     */
1900    public static Number findMaximumRangeValue(XYDataset dataset) {
1901
1902        ParamChecks.nullNotPermitted(dataset, "dataset");
1903
1904        // work out the minimum value...
1905        if (dataset instanceof RangeInfo) {
1906            RangeInfo info = (RangeInfo) dataset;
1907            return new Double(info.getRangeUpperBound(true));
1908        }
1909
1910        // hasn't implemented RangeInfo, so we'll have to iterate...
1911        else  {
1912
1913            double maximum = Double.NEGATIVE_INFINITY;
1914            int seriesCount = dataset.getSeriesCount();
1915            for (int series = 0; series < seriesCount; series++) {
1916                int itemCount = dataset.getItemCount(series);
1917                for (int item = 0; item < itemCount; item++) {
1918                    double value;
1919                    if (dataset instanceof IntervalXYDataset) {
1920                        IntervalXYDataset intervalXYData
1921                                = (IntervalXYDataset) dataset;
1922                        value = intervalXYData.getEndYValue(series, item);
1923                    }
1924                    else if (dataset instanceof OHLCDataset) {
1925                        OHLCDataset highLowData = (OHLCDataset) dataset;
1926                        value = highLowData.getHighValue(series, item);
1927                    }
1928                    else {
1929                        value = dataset.getYValue(series, item);
1930                    }
1931                    if (!Double.isNaN(value)) {
1932                        maximum = Math.max(maximum, value);
1933                    }
1934                }
1935            }
1936            if (maximum == Double.NEGATIVE_INFINITY) {
1937                return null;
1938            }
1939            else {
1940                return new Double(maximum);
1941            }
1942
1943        }
1944
1945    }
1946
1947    /**
1948     * Returns the minimum and maximum values for the dataset's range
1949     * (y-values), assuming that the series in one category are stacked.
1950     *
1951     * @param dataset  the dataset (<code>null</code> not permitted).
1952     *
1953     * @return The range (<code>null</code> if the dataset contains no values).
1954     */
1955    public static Range findStackedRangeBounds(CategoryDataset dataset) {
1956        return findStackedRangeBounds(dataset, 0.0);
1957    }
1958
1959    /**
1960     * Returns the minimum and maximum values for the dataset's range
1961     * (y-values), assuming that the series in one category are stacked.
1962     *
1963     * @param dataset  the dataset (<code>null</code> not permitted).
1964     * @param base  the base value for the bars.
1965     *
1966     * @return The range (<code>null</code> if the dataset contains no values).
1967     */
1968    public static Range findStackedRangeBounds(CategoryDataset dataset,
1969            double base) {
1970        ParamChecks.nullNotPermitted(dataset, "dataset");
1971        Range result = null;
1972        double minimum = Double.POSITIVE_INFINITY;
1973        double maximum = Double.NEGATIVE_INFINITY;
1974        int categoryCount = dataset.getColumnCount();
1975        for (int item = 0; item < categoryCount; item++) {
1976            double positive = base;
1977            double negative = base;
1978            int seriesCount = dataset.getRowCount();
1979            for (int series = 0; series < seriesCount; series++) {
1980                Number number = dataset.getValue(series, item);
1981                if (number != null) {
1982                    double value = number.doubleValue();
1983                    if (value > 0.0) {
1984                        positive = positive + value;
1985                    }
1986                    if (value < 0.0) {
1987                        negative = negative + value;
1988                        // '+', remember value is negative
1989                    }
1990                }
1991            }
1992            minimum = Math.min(minimum, negative);
1993            maximum = Math.max(maximum, positive);
1994        }
1995        if (minimum <= maximum) {
1996            result = new Range(minimum, maximum);
1997        }
1998        return result;
1999
2000    }
2001
2002    /**
2003     * Returns the minimum and maximum values for the dataset's range
2004     * (y-values), assuming that the series in one category are stacked.
2005     *
2006     * @param dataset  the dataset.
2007     * @param map  a structure that maps series to groups.
2008     *
2009     * @return The value range (<code>null</code> if the dataset contains no
2010     *         values).
2011     */
2012    public static Range findStackedRangeBounds(CategoryDataset dataset,
2013            KeyToGroupMap map) {
2014        ParamChecks.nullNotPermitted(dataset, "dataset");
2015        boolean hasValidData = false;
2016        Range result = null;
2017
2018        // create an array holding the group indices for each series...
2019        int[] groupIndex = new int[dataset.getRowCount()];
2020        for (int i = 0; i < dataset.getRowCount(); i++) {
2021            groupIndex[i] = map.getGroupIndex(map.getGroup(
2022                    dataset.getRowKey(i)));
2023        }
2024
2025        // minimum and maximum for each group...
2026        int groupCount = map.getGroupCount();
2027        double[] minimum = new double[groupCount];
2028        double[] maximum = new double[groupCount];
2029
2030        int categoryCount = dataset.getColumnCount();
2031        for (int item = 0; item < categoryCount; item++) {
2032            double[] positive = new double[groupCount];
2033            double[] negative = new double[groupCount];
2034            int seriesCount = dataset.getRowCount();
2035            for (int series = 0; series < seriesCount; series++) {
2036                Number number = dataset.getValue(series, item);
2037                if (number != null) {
2038                    hasValidData = true;
2039                    double value = number.doubleValue();
2040                    if (value > 0.0) {
2041                        positive[groupIndex[series]]
2042                                 = positive[groupIndex[series]] + value;
2043                    }
2044                    if (value < 0.0) {
2045                        negative[groupIndex[series]]
2046                                 = negative[groupIndex[series]] + value;
2047                                 // '+', remember value is negative
2048                    }
2049                }
2050            }
2051            for (int g = 0; g < groupCount; g++) {
2052                minimum[g] = Math.min(minimum[g], negative[g]);
2053                maximum[g] = Math.max(maximum[g], positive[g]);
2054            }
2055        }
2056        if (hasValidData) {
2057            for (int j = 0; j < groupCount; j++) {
2058                result = Range.combine(result, new Range(minimum[j],
2059                        maximum[j]));
2060            }
2061        }
2062        return result;
2063    }
2064
2065    /**
2066     * Returns the minimum value in the dataset range, assuming that values in
2067     * each category are "stacked".
2068     *
2069     * @param dataset  the dataset (<code>null</code> not permitted).
2070     *
2071     * @return The minimum value.
2072     *
2073     * @see #findMaximumStackedRangeValue(CategoryDataset)
2074     */
2075    public static Number findMinimumStackedRangeValue(CategoryDataset dataset) {
2076        ParamChecks.nullNotPermitted(dataset, "dataset");
2077        Number result = null;
2078        boolean hasValidData = false;
2079        double minimum = 0.0;
2080        int categoryCount = dataset.getColumnCount();
2081        for (int item = 0; item < categoryCount; item++) {
2082            double total = 0.0;
2083            int seriesCount = dataset.getRowCount();
2084            for (int series = 0; series < seriesCount; series++) {
2085                Number number = dataset.getValue(series, item);
2086                if (number != null) {
2087                    hasValidData = true;
2088                    double value = number.doubleValue();
2089                    if (value < 0.0) {
2090                        total = total + value;
2091                        // '+', remember value is negative
2092                    }
2093                }
2094            }
2095            minimum = Math.min(minimum, total);
2096        }
2097        if (hasValidData) {
2098            result = new Double(minimum);
2099        }
2100        return result;
2101    }
2102
2103    /**
2104     * Returns the maximum value in the dataset range, assuming that values in
2105     * each category are "stacked".
2106     *
2107     * @param dataset  the dataset (<code>null</code> not permitted).
2108     *
2109     * @return The maximum value (possibly <code>null</code>).
2110     *
2111     * @see #findMinimumStackedRangeValue(CategoryDataset)
2112     */
2113    public static Number findMaximumStackedRangeValue(CategoryDataset dataset) {
2114        ParamChecks.nullNotPermitted(dataset, "dataset");
2115        Number result = null;
2116        boolean hasValidData = false;
2117        double maximum = 0.0;
2118        int categoryCount = dataset.getColumnCount();
2119        for (int item = 0; item < categoryCount; item++) {
2120            double total = 0.0;
2121            int seriesCount = dataset.getRowCount();
2122            for (int series = 0; series < seriesCount; series++) {
2123                Number number = dataset.getValue(series, item);
2124                if (number != null) {
2125                    hasValidData = true;
2126                    double value = number.doubleValue();
2127                    if (value > 0.0) {
2128                        total = total + value;
2129                    }
2130                }
2131            }
2132            maximum = Math.max(maximum, total);
2133        }
2134        if (hasValidData) {
2135            result = new Double(maximum);
2136        }
2137        return result;
2138    }
2139
2140    /**
2141     * Returns the minimum and maximum values for the dataset's range,
2142     * assuming that the series are stacked.
2143     *
2144     * @param dataset  the dataset (<code>null</code> not permitted).
2145     *
2146     * @return The range ([0.0, 0.0] if the dataset contains no values).
2147     */
2148    public static Range findStackedRangeBounds(TableXYDataset dataset) {
2149        return findStackedRangeBounds(dataset, 0.0);
2150    }
2151
2152    /**
2153     * Returns the minimum and maximum values for the dataset's range,
2154     * assuming that the series are stacked, using the specified base value.
2155     *
2156     * @param dataset  the dataset (<code>null</code> not permitted).
2157     * @param base  the base value.
2158     *
2159     * @return The range (<code>null</code> if the dataset contains no values).
2160     */
2161    public static Range findStackedRangeBounds(TableXYDataset dataset,
2162            double base) {
2163        ParamChecks.nullNotPermitted(dataset, "dataset");
2164        double minimum = base;
2165        double maximum = base;
2166        for (int itemNo = 0; itemNo < dataset.getItemCount(); itemNo++) {
2167            double positive = base;
2168            double negative = base;
2169            int seriesCount = dataset.getSeriesCount();
2170            for (int seriesNo = 0; seriesNo < seriesCount; seriesNo++) {
2171                double y = dataset.getYValue(seriesNo, itemNo);
2172                if (!Double.isNaN(y)) {
2173                    if (y > 0.0) {
2174                        positive += y;
2175                    }
2176                    else {
2177                        negative += y;
2178                    }
2179                }
2180            }
2181            if (positive > maximum) {
2182                maximum = positive;
2183            }
2184            if (negative < minimum) {
2185                minimum = negative;
2186            }
2187        }
2188        if (minimum <= maximum) {
2189            return new Range(minimum, maximum);
2190        }
2191        else {
2192            return null;
2193        }
2194    }
2195
2196    /**
2197     * Calculates the total for the y-values in all series for a given item
2198     * index.
2199     *
2200     * @param dataset  the dataset.
2201     * @param item  the item index.
2202     *
2203     * @return The total.
2204     *
2205     * @since 1.0.5
2206     */
2207    public static double calculateStackTotal(TableXYDataset dataset, int item) {
2208        double total = 0.0;
2209        int seriesCount = dataset.getSeriesCount();
2210        for (int s = 0; s < seriesCount; s++) {
2211            double value = dataset.getYValue(s, item);
2212            if (!Double.isNaN(value)) {
2213                total = total + value;
2214            }
2215        }
2216        return total;
2217    }
2218
2219    /**
2220     * Calculates the range of values for a dataset where each item is the
2221     * running total of the items for the current series.
2222     *
2223     * @param dataset  the dataset (<code>null</code> not permitted).
2224     *
2225     * @return The range.
2226     *
2227     * @see #findRangeBounds(CategoryDataset)
2228     */
2229    public static Range findCumulativeRangeBounds(CategoryDataset dataset) {
2230        ParamChecks.nullNotPermitted(dataset, "dataset");
2231        boolean allItemsNull = true; // we'll set this to false if there is at
2232                                     // least one non-null data item...
2233        double minimum = 0.0;
2234        double maximum = 0.0;
2235        for (int row = 0; row < dataset.getRowCount(); row++) {
2236            double runningTotal = 0.0;
2237            for (int column = 0; column <= dataset.getColumnCount() - 1;
2238                 column++) {
2239                Number n = dataset.getValue(row, column);
2240                if (n != null) {
2241                    allItemsNull = false;
2242                    double value = n.doubleValue();
2243                    if (!Double.isNaN(value)) {
2244                        runningTotal = runningTotal + value;
2245                        minimum = Math.min(minimum, runningTotal);
2246                        maximum = Math.max(maximum, runningTotal);
2247                    }
2248                }
2249            }
2250        }
2251        if (!allItemsNull) {
2252            return new Range(minimum, maximum);
2253        }
2254        else {
2255            return null;
2256        }
2257    }
2258
2259    /**
2260     * Returns the interpolated value of y that corresponds to the specified
2261     * x-value in the given series.  If the x-value falls outside the range of
2262     * x-values for the dataset, this method returns <code>Double.NaN</code>.
2263     * 
2264     * @param dataset  the dataset (<code>null</code> not permitted).
2265     * @param series  the series index.
2266     * @param x  the x-value.
2267     * 
2268     * @return The y value.
2269     * 
2270     * @since 1.0.16
2271     */
2272    public static double findYValue(XYDataset dataset, int series, double x) {
2273        // delegate null check on dataset
2274        int[] indices = findItemIndicesForX(dataset, series, x);
2275        if (indices[0] == -1) {
2276            return Double.NaN;
2277        }
2278        if (indices[0] == indices[1]) {
2279            return dataset.getYValue(series, indices[0]);
2280        }
2281        double x0 = dataset.getXValue(series, indices[0]);
2282        double x1 = dataset.getXValue(series, indices[1]);
2283        double y0 = dataset.getYValue(series, indices[0]);
2284        double y1 = dataset.getYValue(series, indices[1]);
2285        return y0 + (y1 - y0) * (x - x0) / (x1 - x0);
2286    }
2287    
2288    /**
2289     * Finds the indices of the the items in the dataset that span the 
2290     * specified x-value.  There are three cases for the return value:
2291     * <ul>
2292     * <li>there is an exact match for the x-value at index i 
2293     * (returns <code>int[] {i, i}</code>);</li>
2294     * <li>the x-value falls between two (adjacent) items at index i and i+1 
2295     * (returns <code>int[] {i, i+1}</code>);</li>
2296     * <li>the x-value falls outside the domain bounds, in which case the 
2297     *    method returns <code>int[] {-1, -1}</code>.</li>
2298     * </ul>
2299     * @param dataset  the dataset (<code>null</code> not permitted).
2300     * @param series  the series index.
2301     * @param x  the x-value.
2302     *
2303     * @return The indices of the two items that span the x-value.
2304     *
2305     * @since 1.0.16
2306     * 
2307     * @see #findYValue(org.jfree.data.xy.XYDataset, int, double) 
2308     */
2309    public static int[] findItemIndicesForX(XYDataset dataset, int series,
2310            double x) {
2311        ParamChecks.nullNotPermitted(dataset, "dataset");
2312        int itemCount = dataset.getItemCount(series);
2313        if (itemCount == 0) {
2314            return new int[] {-1, -1};
2315        }
2316        if (itemCount == 1) {
2317            if (x == dataset.getXValue(series, 0)) {
2318                return new int[] {0, 0};
2319            } else {
2320                return new int[] {-1, -1};
2321            }
2322        }
2323        if (dataset.getDomainOrder() == DomainOrder.ASCENDING) {
2324            int low = 0;
2325            int high = itemCount - 1;
2326            double lowValue = dataset.getXValue(series, low);
2327            if (lowValue > x) {
2328                return new int[] {-1, -1};
2329            }
2330            if (lowValue == x) {
2331                return new int[] {low, low};
2332            }
2333            double highValue = dataset.getXValue(series, high);
2334            if (highValue < x) {
2335                return new int[] {-1, -1};
2336            }
2337            if (highValue == x) {
2338                return new int[] {high, high};
2339            }
2340            int mid = (low + high) / 2;
2341            while (high - low > 1) {
2342                double midV = dataset.getXValue(series, mid);
2343                if (x == midV) {
2344                    return new int[] {mid, mid};
2345                }
2346                if (midV < x) {
2347                    low = mid;
2348                }
2349                else {
2350                    high = mid;
2351                }
2352                mid = (low + high) / 2;
2353            }
2354            return new int[] {low, high};
2355        }
2356        else if (dataset.getDomainOrder() == DomainOrder.DESCENDING) {
2357            int high = 0;
2358            int low = itemCount - 1;
2359            double lowValue = dataset.getXValue(series, low);
2360            if (lowValue > x) {
2361                return new int[] {-1, -1};
2362            }
2363            double highValue = dataset.getXValue(series, high);
2364            if (highValue < x) {
2365                return new int[] {-1, -1};
2366            }
2367            int mid = (low + high) / 2;
2368            while (high - low > 1) {
2369                double midV = dataset.getXValue(series, mid);
2370                if (x == midV) {
2371                    return new int[] {mid, mid};
2372                }
2373                if (midV < x) {
2374                    low = mid;
2375                }
2376                else {
2377                    high = mid;
2378                }
2379                mid = (low + high) / 2;
2380            }
2381            return new int[] {low, high};
2382        }
2383        else {
2384            // we don't know anything about the ordering of the x-values,
2385            // so we iterate until we find the first crossing of x (if any)
2386            // we know there are at least 2 items in the series at this point
2387            double prev = dataset.getXValue(series, 0);
2388            if (x == prev) {
2389                return new int[] {0, 0}; // exact match on first item
2390            }
2391            for (int i = 1; i < itemCount; i++) {
2392                double next = dataset.getXValue(series, i);
2393                if (x == next) {
2394                    return new int[] {i, i}; // exact match
2395                }
2396                if ((x > prev && x < next) || (x < prev && x > next)) {
2397                    return new int[] {i - 1, i}; // spanning match
2398                }
2399            }
2400            return new int[] {-1, -1}; // no crossing of x
2401        }
2402    }
2403
2404}