    001/* ===========================================================
    002 * Orson Charts : a 3D chart library for the Java(tm) platform
    003 * ===========================================================
    004 *
    005 * (C)opyright 2013-2016, by Object Refinery Limited.  All rights reserved.
    006 *
    007 * http://www.object-refinery.com/orsoncharts/index.html
    008 *
    009 * This program is free software: you can redistribute it and/or modify
    010 * it under the terms of the GNU General Public License as published by
    011 * the Free Software Foundation, either version 3 of the License, or
    012 * (at your option) any later version.
    013 *
    014 * This program is distributed in the hope that it will be useful,
    015 * but WITHOUT ANY WARRANTY; without even the implied warranty of
    016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    017 * GNU General Public License for more details.
    018 *
    019 * You should have received a copy of the GNU General Public License
    020 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    021 *
    022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
    023 * Other names may be trademarks of their respective owners.]
    024 *
    025 * If you do not wish to be bound by the terms of the GPL, an alternative
    026 * commercial license can be purchased.  For details, please see visit the
    027 * Orson Charts home page:
    028 *
    029 * http://www.object-refinery.com/orsoncharts/index.html
    030 *
    031 */
    032
    033package com.orsoncharts.data;
    034
    035import java.util.List;
    036import com.orsoncharts.Range;
    037import com.orsoncharts.data.category.CategoryDataset3D;
    038import com.orsoncharts.data.xyz.XYZDataset;
    039import com.orsoncharts.data.xyz.XYZSeries;
    040import com.orsoncharts.data.xyz.XYZSeriesCollection;
    041import com.orsoncharts.util.ArgChecks;
    042
    043/**
    044 * Some utility methods for working with the various datasets and data
    045 * structures available in Orson Charts.
    046 */
    047public class DataUtils {
    048
    049    private DataUtils() {
    050        // no need to create instances
    051    }
    052
    053    /**
    054     * Returns the total of the values in the list.  Any {@code null}
    055     * values are ignored.
    056     *
    057     * @param values  the values ({@code null} not permitted).
    058     *
    059     * @return The total of the values in the list.
    060     */
    061    public static double total(Values<Number> values) {
    062        double result = 0.0;
    063        for (int i = 0; i < values.getItemCount(); i++) {
    064            Number n = values.getValue(i);
    065            if (n != null) {
    066                result = result + n.doubleValue();
    067            }
    068        }
    069        return result;
    070    }
    071
    072    /**
    073     * Returns the count of the non-{@code null} entries in the dataset
    074     * for the specified series.  An {@code IllegalArgumentException} is
    075     * thrown if the {@code seriesKey} is not present in the data.
    076     *
    077     * @param data  the dataset ({@code null} not permitted).
    078     * @param seriesKey  the series key ({@code null} not permitted).
    079     *
    080     * @return The count.
    081     *
    082     * @since 1.2
    083     */
    084    public static int count(KeyedValues3D<?> data, Comparable<?> seriesKey) {
    085        ArgChecks.nullNotPermitted(data, "data");
    086        ArgChecks.nullNotPermitted(seriesKey, "seriesKey");
    087        int seriesIndex = data.getSeriesIndex(seriesKey);
    088        if (seriesIndex < 0) {
    089            throw new IllegalArgumentException("Series not found: "
    090                    + seriesKey);
    091        }
    092        int count = 0;
    093        int rowCount = data.getRowCount();
    094        int columnCount = data.getColumnCount();
    095        for (int r = 0; r < rowCount; r++) {
    096            for (int c = 0; c < columnCount; c++) {
    097                Number n = (Number) data.getValue(seriesIndex, r, c);
    098                if (n != null) {
    099                    count++;
    100                }
    101            }
    102        }
    103        return count;
    104    }
    105
    106    /**
    107     * Returns the count of the non-{@code null} entries in the dataset
    108     * for the specified row (all series).
    109     *
    110     * @param data  the dataset ({@code null} not permitted).
    111     * @param rowKey  the row key ({@code null} not permitted).
    112     *
    113     * @return The count.
    114     *
    115     * @since 1.2
    116     */
    117    public static int countForRow(KeyedValues3D<?> data, Comparable<?> rowKey) {
    118        ArgChecks.nullNotPermitted(data, "data");
    119        ArgChecks.nullNotPermitted(rowKey, "rowKey");
    120        int rowIndex = data.getRowIndex(rowKey);
    121        if (rowIndex < 0) {
    122            throw new IllegalArgumentException("Row not found: " + rowKey);
    123        }
    124        int count = 0;
    125        int seriesCount = data.getSeriesCount();
    126        int columnCount = data.getColumnCount();
    127        for (int s = 0; s < seriesCount; s++) {
    128            for (int c = 0; c < columnCount; c++) {
    129                Number n = (Number) data.getValue(s, rowIndex, c);
    130                if (n != null) {
    131                    count++;
    132                }
    133            }
    134        }
    135        return count;
    136    }
    137
    138    /**
    139     * Returns the count of the non-{@code null} entries in the dataset
    140     * for the specified column (all series).
    141     *
    142     * @param data  the dataset ({@code null} not permitted).
    143     * @param columnKey  the column key ({@code null} not permitted).
    144     *
    145     * @return The count.
    146     *
    147     * @since 1.2
    148     */
    149    public static int countForColumn(KeyedValues3D<?> data,
    150            Comparable<?> columnKey) {
    151        ArgChecks.nullNotPermitted(data, "data");
    152        ArgChecks.nullNotPermitted(columnKey, "columnKey");
    153        int columnIndex = data.getColumnIndex(columnKey);
    154        if (columnIndex < 0) {
    155            throw new IllegalArgumentException("Column not found: "
    156                    + columnKey);
    157        }
    158        int count = 0;
    159        int seriesCount = data.getSeriesCount();
    160        int rowCount = data.getRowCount();
    161        for (int s = 0; s < seriesCount; s++) {
    162            for (int r = 0; r < rowCount; r++) {
    163                Number n = (Number) data.getValue(s, r, columnIndex);
    164                if (n != null) {
    165                    count++;
    166                }
    167            }
    168        }
    169        return count;
    170    }
    171
    172    /**
    173     * Returns the total of the non-{@code null} values in the dataset
    174     * for the specified series.  If there is no series with the specified
    175     * key, this method will throw an {@code IllegalArgumentException}.
    176     *
    177     * @param data  the dataset ({@code null} not permitted).
    178     * @param seriesKey  the series key ({@code null} not permitted).
    179     *
    180     * @return The total.
    181     *
    182     * @since 1.2
    183     */
    184    public static double total(KeyedValues3D<? extends Number> data,
    185            Comparable<?> seriesKey) {
    186        ArgChecks.nullNotPermitted(data, "data");
    187        ArgChecks.nullNotPermitted(seriesKey, "seriesKey");
    188        int seriesIndex = data.getSeriesIndex(seriesKey);
    189        if (seriesIndex < 0) {
    190            throw new IllegalArgumentException("Series not found: "
    191                    + seriesKey);
    192        }
    193        double total = 0.0;
    194        int rowCount = data.getRowCount();
    195        int columnCount = data.getColumnCount();
    196        for (int r = 0; r < rowCount; r++) {
    197            for (int c = 0; c < columnCount; c++) {
    198                Number n = (Number) data.getValue(seriesIndex, r, c);
    199                if (n != null) {
    200                    total += n.doubleValue();
    201                }
    202            }
    203        }
    204        return total;
    205    }
    206
    207    /**
    208     * Returns the total of the non-{@code null} entries in the dataset
    209     * for the specified row (all series).
    210     *
    211     * @param data  the dataset ({@code null} not permitted).
    212     * @param rowKey  the row key ({@code null} not permitted).
    213     *
    214     * @return The total.
    215     *
    216     * @since 1.2
    217     */
    218    public static double totalForRow(KeyedValues3D<? extends Number> data,
    219            Comparable<?> rowKey) {
    220        ArgChecks.nullNotPermitted(data, "data");
    221        ArgChecks.nullNotPermitted(rowKey, "rowKey");
    222        int rowIndex = data.getRowIndex(rowKey);
    223        if (rowIndex < 0) {
    224            throw new IllegalArgumentException("Row not found: " + rowKey);
    225        }
    226        double total = 0.0;
    227        int seriesCount = data.getSeriesCount();
    228        int columnCount = data.getColumnCount();
    229        for (int s = 0; s < seriesCount; s++) {
    230            for (int c = 0; c < columnCount; c++) {
    231                Number n = (Number) data.getValue(s, rowIndex, c);
    232                if (n != null) {
    233                    total += n.doubleValue();
    234                }
    235            }
    236        }
    237        return total;
    238    }
    239
    240    /**
    241     * Returns the total of the non-{@code null} entries in the dataset
    242     * for the specified column (all series).
    243     *
    244     * @param data  the dataset ({@code null} not permitted).
    245     * @param columnKey  the row key ({@code null} not permitted).
    246     *
    247     * @return The total.
    248     *
    249     * @since 1.2
    250     */
    251    public static double totalForColumn(KeyedValues3D<? extends Number> data,
    252            Comparable<?> columnKey) {
    253        ArgChecks.nullNotPermitted(data, "data");
    254        ArgChecks.nullNotPermitted(columnKey, "columnKey");
    255        int columnIndex = data.getColumnIndex(columnKey);
    256        if (columnIndex < 0) {
    257            throw new IllegalArgumentException("Column not found: "
    258                    + columnKey);
    259        }
    260        double total = 0.0;
    261        int seriesCount = data.getSeriesCount();
    262        int rowCount = data.getRowCount();
    263        for (int s = 0; s < seriesCount; s++) {
    264            for (int r = 0; r < rowCount; r++) {
    265                Number n = (Number) data.getValue(s, r, columnIndex);
    266                if (n != null) {
    267                    total += n.doubleValue();
    268                }
    269            }
    270        }
    271        return total;
    272    }
    273
    274    /**
    275     * Returns the range of values in the specified data structure (a three
    276     * dimensional cube).  If there is no data, this method returns
    277     * {@code null}.
    278     *
    279     * @param data  the data ({@code null} not permitted).
    280     *
    281     * @return The range of data values (possibly {@code null}).
    282     */
    283    public static Range findValueRange(Values3D<? extends Number> data) {
    284        return findValueRange(data, Double.NaN);
    285    }
    286
    287    /**
    288     * Returns the range of values in the specified data cube, or
    289     * {@code null} if there is no data.  The range will be expanded, if
    290     * required, to include the {@code base} value (unless it
    291     * is {@code Double.NaN} in which case it is ignored).
    292     *
    293     * @param data  the data ({@code null} not permitted).
    294     * @param base  a value that must be included in the range (often 0).  This
    295     *         argument is ignored if it is {@code Double.NaN}.
    296     *
    297     * @return The range (possibly {@code null}).
    298     */
    299    public static Range findValueRange(Values3D<? extends Number> data,
    300            double base) {
    301        return findValueRange(data, base, true);
    302    }
    303
    304    /**
    305    /**
    306     * Returns the range of values in the specified data cube, or
    307     * {@code null} if there is no data.  The range will be expanded, if
    308     * required, to include the {@code base} value (unless it
    309     * is {@code Double.NaN} in which case it is ignored).
    310     *
    311     * @param data  the data ({@code null} not permitted).
    312     * @param base  a value that must be included in the range (often 0).  This
    313     *         argument is ignored if it is {@code Double.NaN}.
    314     * @param finite  if {@code true} infinite values will be ignored.
    315     *
    316     * @return The range (possibly {@code null}).
    317     *
    318     * @since 1.4
    319     */
    320    public static Range findValueRange(Values3D<? extends Number> data,
    321            double base, boolean finite) {
    322        ArgChecks.nullNotPermitted(data, "data");
    323        double min = Double.POSITIVE_INFINITY;
    324        double max = Double.NEGATIVE_INFINITY;
    325        for (int series = 0; series < data.getSeriesCount(); series++) {
    326            for (int row = 0; row < data.getRowCount(); row++) {
    327                for (int col = 0; col < data.getColumnCount(); col++) {
    328                    double d = data.getDoubleValue(series, row, col);
    329                    if (!Double.isNaN(d)) {
    330                        if (!finite || !Double.isInfinite(d)) {
    331                            min = Math.min(min, d);
    332                            max = Math.max(max, d);
    333                        }
    334                    }
    335                }
    336            }
    337        }
    338        // include the special value in the range
    339        if (!Double.isNaN(base)) {
    340             min = Math.min(min, base);
    341             max = Math.max(max, base);
    342        }
    343        if (min <= max) {
    344            return new Range(min, max);
    345        } else {
    346            return null;
    347        }
    348    }
    349
    350    /**
    351     * Finds the range of values in the dataset considering that each series
    352     * is stacked on top of the other.
    353     *
    354     * @param data  the data ({@code null} not permitted).
    355     *
    356     * @return The range.
    357     */
    358    public static Range findStackedValueRange(Values3D<? extends Number> data) {
    359        return findStackedValueRange(data, 0.0);
    360    }
    361
    362    /**
    363     * Finds the range of values in the dataset considering that each series
    364     * is stacked on top of the others, starting at the base value.
    365     *
    366     * @param data  the data values ({@code null} not permitted).
    367     * @param base  the base value.
    368     *
    369     * @return The range.
    370     */
    371    public static Range findStackedValueRange(Values3D<? extends Number> data,
    372            double base) {
    373        ArgChecks.nullNotPermitted(data, "data");
    374        double min = base;
    375        double max = base;
    376        int seriesCount = data.getSeriesCount();
    377        for (int row = 0; row < data.getRowCount(); row++) {
    378            for (int col = 0; col < data.getColumnCount(); col++) {
    379                double[] total = stackSubTotal(data, base, seriesCount, row,
    380                        col);
    381                min = Math.min(min, total[0]);
    382                max = Math.max(max, total[1]);
    383            }
    384        }
    385        if (min <= max) {
    386            return new Range(min, max);
    387        } else {
    388            return null;
    389        }
    390    }
    391
    392    /**
    393     * Returns the positive and negative subtotals of the values for all the
    394     * series preceding the specified series.
    395     * <br><br>
    396     * One application for this method is to compute the base values for
    397     * individual bars in a stacked bar chart.
    398     *
    399     * @param data  the data ({@code null} not permitted).
    400     * @param base  the initial base value (normally {@code 0.0}, but the
    401     *     values can be stacked from a different starting point).
    402     * @param series  the index of the current series (series with lower indices
    403     *     are included in the sub-totals).
    404     * @param row  the row index of the required item.
    405     * @param column  the column index of the required item.
    406     *
    407     * @return The subtotals, where {@code result[0]} is the subtotal of
    408     *     the negative data items, and {@code result[1]} is the subtotal
    409     *     of the positive data items.
    410     */
    411    public static double[] stackSubTotal(Values3D<? extends Number> data,
    412            double base, int series, int row, int column) {
    413        double neg = base;
    414        double pos = base;
    415        for (int s = 0; s < series; s++) {
    416            double v = data.getDoubleValue(s, row, column);
    417            if (v > 0.0) {
    418                pos = pos + v;
    419            } else if (v < 0.0) {
    420                neg = neg + v;
    421            }
    422        }
    423        return new double[] { neg, pos };
    424    }
    425
    426    /**
    427     * Returns the total of the non-{@code NaN} entries in the dataset
    428     * for the specified series.
    429     *
    430     * @param data  the dataset ({@code null} not permitted).
    431     * @param seriesKey  the series key ({@code null} not permitted).
    432     *
    433     * @return The count.
    434     *
    435     * @since 1.2
    436     */
    437    public static double total(XYZDataset data, Comparable<?> seriesKey) {
    438        ArgChecks.nullNotPermitted(data, "data");
    439        ArgChecks.nullNotPermitted(seriesKey, "seriesKey");
    440        int seriesIndex = data.getSeriesIndex(seriesKey);
    441        if (seriesIndex < 0) {
    442            throw new IllegalArgumentException("Series not found: "
    443                    + seriesKey);
    444        }
    445        double total = 0;
    446        int itemCount = data.getItemCount(seriesIndex);
    447        for (int item = 0; item < itemCount; item++) {
    448            double y = data.getY(seriesIndex, item);
    449            if (!Double.isNaN(y)) {
    450                total += y;
    451            }
    452        }
    453        return total;
    454    }
    455
    456    /**
    457     * Returns the range of x-values in the dataset by iterating over all
    458     * values (and ignoring {@code Double.NaN} and infinite values).
    459     * If there are no values eligible for inclusion in the range, this method
    460     * returns {@code null}.
    461     *
    462     * @param dataset  the dataset ({@code null} not permitted).
    463     *
    464     * @return The range (possibly {@code null}).
    465     */
    466    public static Range findXRange(XYZDataset dataset) {
    467        return findXRange(dataset, Double.NaN);
    468    }
    469
    470    /**
    471     * Returns the range of x-values in the dataset by iterating over all
    472     * values (and ignoring {@code Double.NaN} values).  The range will be
    473     * extended if necessary to include {@code inc} (unless it is
    474     * {@code Double.NaN} in which case it is ignored).  Infinite values
    475     * in the dataset will be ignored.  If there are no values eligible for
    476     * inclusion in the range, this method returns {@code null}.
    477     *
    478     * @param dataset  the dataset ({@code null} not permitted).
    479     * @param inc  an additional x-value to include.
    480     *
    481     * @return The range (possibly {@code null}).
    482     */
    483    public static Range findXRange(XYZDataset dataset, double inc) {
    484        return findXRange(dataset, inc, true);
    485    }
    486
    487    /**
    488     * Returns the range of x-values in the dataset by iterating over all
    489     * values (and ignoring {@code Double.NaN} values).  The range will be
    490     * extended if necessary to include {@code inc} (unless it is
    491     * {@code Double.NaN} in which case it is ignored).  If the
    492     * {@code finite} flag is set, infinite values in the dataset will be
    493     * ignored.  If there are no values eligible for inclusion in the range,
    494     * this method returns {@code null}.
    495     *
    496     * @param dataset  the dataset ({@code null} not permitted).
    497     * @param inc  an additional x-value to include.
    498     * @param finite  a flag indicating whether to exclude infinite values.
    499     *
    500     * @return The range (possibly {@code null}).
    501     *
    502     * @since 1.4
    503     */
    504    public static Range findXRange(XYZDataset dataset, double inc,
    505            boolean finite) {
    506        ArgChecks.nullNotPermitted(dataset, "dataset");
    507        double min = Double.POSITIVE_INFINITY;
    508        double max = Double.NEGATIVE_INFINITY;
    509        for (int s = 0; s < dataset.getSeriesCount(); s++) {
    510            for (int i = 0; i < dataset.getItemCount(s); i++) {
    511                double x = dataset.getX(s, i);
    512                if (!Double.isNaN(x)) {
    513                    if (!finite || !Double.isInfinite(x)) {
    514                        min = Math.min(x, min);
    515                        max = Math.max(x, max);
    516                    }
    517                }
    518            }
    519        }
    520        if (!Double.isNaN(inc)) {
    521            min = Math.min(inc, min);
    522            max = Math.max(inc, max);
    523        }
    524        if (min <= max) {
    525            return new Range(min, max);
    526        } else {
    527            return null;
    528        }
    529    }
    530
    531    /**
    532     * Returns the range of y-values in the dataset by iterating over all
    533     * values (and ignoring {@code Double.NaN} and infinite values).
    534     * If there are no values eligible for inclusion in the range, this method
    535     * returns {@code null}.
    536     *
    537     * @param dataset  the dataset ({@code null} not permitted).
    538     *
    539     * @return The range.
    540     */
    541    public static Range findYRange(XYZDataset dataset) {
    542        return findYRange(dataset, Double.NaN);
    543    }
    544
    545    /**
    546     * Returns the range of y-values in the dataset by iterating over all
    547     * values (and ignoring {@code Double.NaN} values).  The range will be
    548     * extended if necessary to include {@code inc} (unless it is
    549     * {@code Double.NaN} in which case it is ignored).  Infinite values
    550     * in the dataset will be ignored.  If there are no values eligible for
    551     * inclusion in the range, this method returns {@code null}.
    552     *
    553     * @param dataset  the dataset ({@code null} not permitted).
    554     * @param inc  an additional x-value to include.
    555     *
    556     * @return The range.
    557     */
    558    public static Range findYRange(XYZDataset dataset, double inc) {
    559        return findYRange(dataset, inc, true);
    560    }
    561
    562    /**
    563     * Returns the range of y-values in the dataset by iterating over all
    564     * values (and ignoring {@code Double.NaN} values).  The range will be
    565     * extended if necessary to include {@code inc} (unless it is
    566     * {@code Double.NaN} in which case it is ignored).  If the
    567     * {@code finite} flag is set, infinite values in the dataset will be
    568     * ignored.  If there are no values eligible for inclusion in the range,
    569     * this method returns {@code null}.
    570     *
    571     * @param dataset  the dataset ({@code null} not permitted).
    572     * @param inc  an additional y-value to include.
    573     * @param finite  a flag indicating whether to exclude infinite values.
    574     *
    575     * @return The range (possibly {@code null}).
    576     *
    577     * @since 1.4
    578     */
    579    public static Range findYRange(XYZDataset dataset, double inc,
    580            boolean finite) {
    581        ArgChecks.nullNotPermitted(dataset, "dataset");
    582        double min = Double.POSITIVE_INFINITY;
    583        double max = Double.NEGATIVE_INFINITY;
    584        for (int s = 0; s < dataset.getSeriesCount(); s++) {
    585            for (int i = 0; i < dataset.getItemCount(s); i++) {
    586                double y = dataset.getY(s, i);
    587                if (!Double.isNaN(y)) {
    588                    if (!finite || !Double.isInfinite(y)) {
    589                        min = Math.min(y, min);
    590                        max = Math.max(y, max);
    591                    }
    592                }
    593            }
    594        }
    595        if (!Double.isNaN(inc)) {
    596            min = Math.min(inc, min);
    597            max = Math.max(inc, max);
    598        }
    599        if (min <= max) {
    600            return new Range(min, max);
    601        } else {
    602            return null;
    603        }
    604    }
    605
    606    /**
    607     * Returns the range of z-values in the dataset by iterating over all
    608     * values (and ignoring {@code Double.NaN} and infinite values).
    609     * If there are no values eligible for inclusion in the range, this method
    610     * returns {@code null}.
    611     *
    612     * @param dataset  the dataset ({@code null} not permitted).
    613     *
    614     * @return The range (possibly {@code null}).
    615     */
    616    public static Range findZRange(XYZDataset dataset) {
    617        return findZRange(dataset, Double.NaN);
    618    }
    619
    620    /**
    621     * Returns the range of z-values in the dataset by iterating over all
    622     * values (and ignoring {@code Double.NaN} values).  The range will be
    623     * extended if necessary to include {@code inc} (unless it is
    624     * {@code Double.NaN} in which case it is ignored).  Infinite values
    625     * in the dataset will be ignored.  If there are no values eligible for
    626     * inclusion in the range, this method returns {@code null}.
    627     *
    628     * @param dataset  the dataset ({@code null} not permitted).
    629     * @param inc  an additional x-value to include.
    630     *
    631     * @return The range (possibly {@code null}).
    632     */
    633    public static Range findZRange(XYZDataset dataset, double inc) {
    634        return findZRange(dataset, inc, true);
    635    }
    636
    637    /**
    638     * Returns the range of z-values in the dataset by iterating over all
    639     * values (and ignoring {@code Double.NaN} values).  The range will be
    640     * extended if necessary to include {@code inc} (unless it is
    641     * {@code Double.NaN} in which case it is ignored).  If the
    642     * {@code finite} flag is set, infinite values in the dataset will be
    643     * ignored.  If there are no values eligible for inclusion in the range,
    644     * this method returns {@code null}.
    645     *
    646     * @param dataset  the dataset ({@code null} not permitted).
    647     * @param inc  an additional z-value to include.
    648     * @param finite  a flag indicating whether to exclude infinite values.
    649     *
    650     * @return The range (possibly {@code null}).
    651     *
    652     * @since 1.4
    653     */
    654    public static Range findZRange(XYZDataset dataset, double inc,
    655            boolean finite) {
    656        ArgChecks.nullNotPermitted(dataset, "dataset");
    657        ArgChecks.finiteRequired(inc, "inc");
    658        double min = Double.POSITIVE_INFINITY;
    659        double max = Double.NEGATIVE_INFINITY;
    660        for (int s = 0; s < dataset.getSeriesCount(); s++) {
    661            for (int i = 0; i < dataset.getItemCount(s); i++) {
    662                double z = dataset.getZ(s, i);
    663                if (!Double.isNaN(z)) {
    664                    if (!finite || !Double.isInfinite(z)) {
    665                        min = Math.min(z, min);
    666                        max = Math.max(z, max);
    667                    }
    668                }
    669            }
    670        }
    671        if (!Double.isNaN(inc)) {
    672            min = Math.min(inc, min);
    673            max = Math.max(inc, max);
    674        }
    675        if (min <= max) {
    676            return new Range(min, max);
    677        } else {
    678            return null;
    679        }
    680    }
    681
    682    /**
    683     * Creates an {@link XYZDataset} by extracting values from specified
    684     * rows in a {@link KeyedValues3D} instance, across all the available
    685     * columns (items where any of the x, y or z values is {@code null}
    686     * are skipped).  The new dataset contains a copy of the data and is
    687     * completely independent of the {@code source} dataset.
    688     * <br><br>
    689     * Note that {@link CategoryDataset3D} is an extension of
    690     * {@link KeyedValues3D} so you can use this method for any implementation
    691     * of the {@code CategoryDataset3D} interface.
    692     *
    693     * @param source  the source data ({@code null} not permitted).
    694     * @param xRowKey  the row key for x-values ({@code null} not
    695     *         permitted).
    696     * @param yRowKey  the row key for y-values ({@code null} not
    697     *         permitted).
    698     * @param zRowKey  the row key for z-values ({@code null} not
    699     *         permitted).
    700     *
    701     * @return A new dataset.
    702     *
    703     * @since 1.3
    704     */
    705    public static XYZDataset extractXYZDatasetFromRows(
    706            KeyedValues3D<? extends Number> source,
    707            Comparable<?> xRowKey, Comparable<?> yRowKey,
    708            Comparable<?> zRowKey) {
    709        return extractXYZDatasetFromColumns(source, xRowKey, yRowKey, zRowKey,
    710                NullConversion.SKIP, null);
    711    }
    712
    713    /**
    714     * Creates an {@link XYZDataset} by extracting values from specified
    715     * rows in a {@link KeyedValues3D} instance.  The new dataset contains
    716     * a copy of the data and is completely independent of the
    717     * {@code source} dataset.  Note that {@link CategoryDataset3D} is an
    718     * extension of {@link KeyedValues3D}.
    719     * <br><br>
    720     * Special handling is provided for items that contain {@code null}
    721     * values.  The caller may pass in an {@code exceptions} list (
    722     * normally empty) that will be populated with the keys of the items that
    723     * receive special handling, if any.
    724     *
    725     * @param source  the source data ({@code null} not permitted).
    726     * @param xRowKey  the row key for x-values ({@code null} not
    727     *         permitted).
    728     * @param yRowKey  the row key for y-values ({@code null} not
    729     *         permitted).
    730     * @param zRowKey  the row key for z-values ({@code null} not
    731     *         permitted).
    732     * @param nullConversion  specifies the treatment for {@code null}
    733     *         values in the dataset ({@code null} not permitted).
    734     * @param exceptions  a list that, if not null, will be populated with
    735     *         keys for the items in the source dataset that contain
    736     *         {@code null} values ({@code null} permitted).
    737     *
    738     * @return A new dataset.
    739     *
    740     * @since 1.3
    741     */
    742    public static XYZDataset extractXYZDatasetFromRows(
    743            KeyedValues3D<? extends Number> source,
    744            Comparable<?> xRowKey, Comparable<?> yRowKey,
    745            Comparable<?> zRowKey, NullConversion nullConversion,
    746            List<KeyedValues3DItemKey> exceptions) {
    747
    748        ArgChecks.nullNotPermitted(source, "source");
    749        ArgChecks.nullNotPermitted(xRowKey, "xRowKey");
    750        ArgChecks.nullNotPermitted(yRowKey, "yRowKey");
    751        ArgChecks.nullNotPermitted(zRowKey, "zRowKey");
    752        XYZSeriesCollection dataset = new XYZSeriesCollection();
    753        for (Comparable<?> seriesKey : source.getSeriesKeys()) {
    754            XYZSeries series = new XYZSeries(seriesKey);
    755            for (Comparable<?> colKey : source.getColumnKeys()) {
    756                Number x = source.getValue(seriesKey, xRowKey, colKey);
    757                Number y = source.getValue(seriesKey, yRowKey, colKey);
    758                Number z = source.getValue(seriesKey, zRowKey, colKey);
    759                if (x != null && y != null && z != null) {
    760                    series.add(x.doubleValue(), y.doubleValue(),
    761                            z.doubleValue());
    762                } else {
    763                    if (exceptions != null) {
    764                        // add only one exception per data value
    765                        Comparable rrKey = zRowKey;
    766                        if (x == null) {
    767                            rrKey = xRowKey;
    768                        } else if (y == null) {
    769                            rrKey = yRowKey;
    770                        }
    771                        exceptions.add(new KeyedValues3DItemKey(seriesKey,
    772                                rrKey, colKey));
    773                    }
    774                    if (nullConversion.equals(NullConversion.THROW_EXCEPTION)) {
    775                        Comparable rrKey = zRowKey;
    776                        if (x == null) {
    777                            rrKey = yRowKey;
    778                        } else if (y == null) {
    779                            rrKey = yRowKey;
    780                        }
    781                        throw new RuntimeException("There is a null value for "
    782                                + "the item [" + seriesKey +", " + rrKey + ", "
    783                                + colKey + "].");
    784                    }
    785                    if (nullConversion != NullConversion.SKIP) {
    786                        double xx = convert(x, nullConversion);
    787                        double yy = convert(y, nullConversion);
    788                        double zz = convert(z, nullConversion);
    789                        series.add(xx, yy, zz);
    790                    }
    791                }
    792            }
    793            dataset.add(series);
    794        }
    795        return dataset;
    796    }
    797
    798    /**
    799     * Creates an {@link XYZDataset} by extracting values from specified
    800     * columns in a {@link KeyedValues3D} instance, across all the available
    801     * rows (items where any of the x, y or z values is {@code null} are
    802     * skipped).  The new dataset contains a copy of the data and is completely
    803     * independent of the {@code source} dataset.
    804     * <br><br>
    805     * Note that {@link CategoryDataset3D} is an extension of
    806     * {@link KeyedValues3D} so you can use this method for any implementation
    807     * of the {@code CategoryDataset3D} interface.
    808     *
    809     * @param source  the source data ({@code null} not permitted).
    810     * @param xColKey  the column key for x-values ({@code null} not
    811     *         permitted).
    812     * @param yColKey  the column key for y-values ({@code null} not
    813     *         permitted).
    814     * @param zColKey  the column key for z-values ({@code null} not
    815     *         permitted).
    816     *
    817     * @return A new dataset.
    818     *
    819     * @since 1.3
    820     */
    821    public static XYZDataset extractXYZDatasetFromColumns(
    822            KeyedValues3D<? extends Number> source,
    823            Comparable<?> xColKey, Comparable<?> yColKey,
    824            Comparable<?> zColKey) {
    825        return extractXYZDatasetFromColumns(source, xColKey, yColKey, zColKey,
    826                NullConversion.SKIP, null);
    827    }
    828
    829    /**
    830     * Creates an {@link XYZDataset} by extracting values from specified
    831     * columns in a {@link KeyedValues3D} instance.  The new dataset contains
    832     * a copy of the data and is completely independent of the
    833     * {@code source} dataset.  Note that {@link CategoryDataset3D} is an
    834     * extension of {@link KeyedValues3D}.
    835     * <br><br>
    836     * Special handling is provided for items that contain {@code null}
    837     * values.  The caller may pass in an {@code exceptions} list (
    838     * normally empty) that will be populated with the keys of the items that
    839     * receive special handling, if any.
    840     *
    841     * @param source  the source data ({@code null} not permitted).
    842     * @param xColKey  the column key for x-values ({@code null} not
    843     *         permitted).
    844     * @param yColKey  the column key for y-values ({@code null} not
    845     *         permitted).
    846     * @param zColKey  the column key for z-values ({@code null} not
    847     *         permitted).
    848     * @param nullConversion  specifies the treatment for {@code null}
    849     *         values in the dataset ({@code null} not permitted).
    850     * @param exceptions  a list that, if not null, will be populated with
    851     *         keys for the items in the source dataset that contain
    852     *         {@code null} values ({@code null} permitted).
    853     *
    854     * @return A new dataset.
    855     *
    856     * @since 1.3
    857     */
    858    public static XYZDataset extractXYZDatasetFromColumns(
    859            KeyedValues3D<? extends Number> source,
    860            Comparable<?> xColKey, Comparable<?> yColKey,
    861            Comparable<?> zColKey, NullConversion nullConversion,
    862            List<KeyedValues3DItemKey> exceptions) {
    863
    864        ArgChecks.nullNotPermitted(source, "source");
    865        ArgChecks.nullNotPermitted(xColKey, "xColKey");
    866        ArgChecks.nullNotPermitted(yColKey, "yColKey");
    867        ArgChecks.nullNotPermitted(zColKey, "zColKey");
    868        XYZSeriesCollection dataset = new XYZSeriesCollection();
    869        for (Comparable<?> seriesKey : source.getSeriesKeys()) {
    870            XYZSeries series = new XYZSeries(seriesKey);
    871            for (Comparable<?> rowKey : source.getRowKeys()) {
    872                Number x = source.getValue(seriesKey, rowKey, xColKey);
    873                Number y = source.getValue(seriesKey, rowKey, yColKey);
    874                Number z = source.getValue(seriesKey, rowKey, zColKey);
    875                if (x != null && y != null && z != null) {
    876                    series.add(x.doubleValue(), y.doubleValue(),
    877                            z.doubleValue());
    878                } else {
    879                    if (exceptions != null) {
    880                        // add only one key ref out of the possible 3 per item
    881                        Comparable<?> ccKey = zColKey;
    882                        if (x == null) {
    883                            ccKey = xColKey;
    884                        } else if (y == null) {
    885                            ccKey = yColKey;
    886                        }
    887                        exceptions.add(new KeyedValues3DItemKey(seriesKey,
    888                                rowKey, ccKey));
    889                    }
    890                    if (nullConversion.equals(NullConversion.THROW_EXCEPTION)) {
    891                        Comparable<?> ccKey = zColKey;
    892                        if (x == null) {
    893                            ccKey = xColKey;
    894                        } else if (y == null) {
    895                            ccKey = yColKey;
    896                        }
    897                        throw new RuntimeException("There is a null value for "
    898                                + "the item [" + seriesKey +", " + rowKey + ", "
    899                                + ccKey + "].");
    900                    }
    901                    if (nullConversion != NullConversion.SKIP) {
    902                        double xx = convert(x, nullConversion);
    903                        double yy = convert(y, nullConversion);
    904                        double zz = convert(z, nullConversion);
    905                        series.add(xx, yy, zz);
    906                    }
    907                }
    908            }
    909            dataset.add(series);
    910        }
    911        return dataset;
    912    }
    913
    914    /**
    915     * Returns a double primitive for the specified number, with
    916     * {@code null} values returning {@code Double.NaN} except in the
    917     * case of {@code CONVERT_TO_ZERO} which returns 0.0.  Note that this
    918     * method does not throw an exception for {@code THROW_EXCEPTION}, it
    919     * expects code higher up the call chain to handle that (because there is
    920     * not enough information here to throw a useful exception).
    921     *
    922     * @param n  the number ({@code null} permitted).
    923     * @param nullConversion  the null conversion ({@code null} not
    924     *         permitted).
    925     *
    926     * @return A double primitive.
    927     */
    928    private static double convert(Number n, NullConversion nullConversion) {
    929        if (n != null) {
    930            return n.doubleValue();
    931        } else {
    932            if (nullConversion.equals(NullConversion.CONVERT_TO_ZERO)) {
    933                return 0.0;
    934            }
    935            return Double.NaN;
    936        }
    937    }
    938
    939}